Mock reference
The convex-angular/testing entry point exports two runtime values and seven types:
import { MockConvexClient, provideConvexTesting, type MockAuthRegistration, type MockCallableCall, type MockConvexClientOptions, type MockLocalQueryResultCall, type MockPaginatedSubscription, type MockQuerySubscription, type MockRefusedSubscription,} from 'convex-angular/testing';provideConvexTesting()
Section titled “provideConvexTesting()”function provideConvexTesting(client?: MockConvexClient): Provider[];The parameter defaults to a fresh instance:
function provideConvexTesting(client: MockConvexClient = new MockConvexClient()): Provider[];It returns a plain Provider[] — not EnvironmentProviders, unlike provideConvex(). That
makes it usable anywhere a provider array is accepted, including TestBed.configureTestingModule,
a component-level providers array, and Injector.create.
It registers only the CONVEX token, pointing at the supplied mock:
[{ provide: CONVEX, useValue: client as unknown as ConvexClient }];Nothing else that provideConvex() registers is included — no SSR configuration, no
ConvexHttpClient, no server query loader, no hydration state, and no duplicate-registration guard.
Helpers inject those services optionally, which is exactly why a bare CONVEX token is enough for a
test. Auth providers (provideConvexAuth(), provideClerkAuth(), provideAuth0Auth(),
provideBetterAuth()) are separate and must be registered yourself if the code under test needs
them.
MockConvexClient
Section titled “MockConvexClient”class MockConvexClient { constructor(options: MockConvexClientOptions = {});}MockConvexClientOptions
Section titled “MockConvexClientOptions”interface MockConvexClientOptions { disabled?: boolean;}| Option | Type | Default | Meaning |
|---|---|---|---|
disabled |
boolean |
false |
Mirror a disabled ConvexClient (the server-render state): subscriptions become no-ops, the client getter throws, and mutation()/action()/query() reject with 'ConvexClient is disabled', exactly like the real client. |
Capture properties
Section titled “Capture properties”Every array is readonly, ordered oldest first, and never cleared — build a fresh
MockConvexClient per test.
| Property | Type | Contents |
|---|---|---|
querySubscriptions |
MockQuerySubscription[] |
Every live-query subscription made through onUpdate. |
paginatedSubscriptions |
MockPaginatedSubscription[] |
Every paginated subscription. |
mutationCalls |
MockCallableCall[] |
Every mutation invocation. |
actionCalls |
MockCallableCall[] |
Every action invocation. |
authRegistrations |
MockAuthRegistration[] |
Every auth registration made through client.setAuth. |
localQueryResultCalls |
MockLocalQueryResultCall[] |
Every client.localQueryResult warm-cache lookup, hit or miss. |
refusedSubscriptions |
MockRefusedSubscription[] |
Every subscription a disabled client skipped rather than established. Always empty on an enabled mock. |
Getters
Section titled “Getters”| Getter | Type | Behavior |
|---|---|---|
disabled |
boolean |
options.disabled ?? false. |
client |
object | The low-level client surface used by the helpers. Throws Error('ConvexClient is disabled') when disabled is true, mirroring the real client. |
clearAuthCount |
number |
How many times the code under test called client.clearAuth(). |
hasAuthCount |
number |
How many times it consulted client.hasAuth(). Distinguishes “asked and declined to clear” from “never looked”. |
connectionStateReads |
number |
How many times it read connectionState(). Counted before the disabled throw, so the attempt is recorded either way. |
connectionStateSubscriptions |
number |
How many times it called subscribeToConnectionState(), including calls a disabled client counted but never registered. |
connectionStateUnsubscribes |
number |
How many times it released a connection-state subscription. |
The client getter returns the mock’s own low-level client object — the same instance every time, so
jest.spyOn(convex.client, 'setAuth') sticks. It exposes exactly four members:
| Member | Behavior |
|---|---|
localQueryResult(queryName: string, args?: Record<string, unknown>) |
Records the lookup on localQueryResultCalls, then reads the warm cache populated by seedQueryResult(); undefined on a miss. |
setAuth(fetchToken, onChange?, onRefreshChange?) |
Pushes a MockAuthRegistration onto authRegistrations. Registration is configuration — it does not make the client authenticated. |
clearAuth() |
Increments clearAuthCount, drops the held token, and marks every registration cleared. |
hasAuth() |
Increments hasAuthCount and reports whether a token is held — that is, whether seedAuth() gave it one that has not been cleared. |
Test-driving methods
Section titled “Test-driving methods”| Method | Returns | Purpose |
|---|---|---|
lastQuerySubscription() |
MockQuerySubscription | undefined |
The most recent live-query subscription, if any. |
lastPaginatedSubscription() |
MockPaginatedSubscription | undefined |
The most recent paginated subscription, if any. |
lastAuthRegistration() |
MockAuthRegistration | undefined |
The most recent auth registration, if any. |
seedQueryResult(queryName: string, args: Record<string, unknown>, result: unknown) |
void |
Pre-seed the warm local cache consulted by injectQuery/injectQueries before their subscription delivers. |
seedAuth(auth: { token, decoded } | undefined) |
void |
Give the client a token. The single source of current auth: it drives both getAuth() and client.hasAuth(). |
setConnectionState(state: Partial<ConnectionState>) |
void |
Shallow-merge into the current state and push it to every connection-state listener. |
ConvexClient surface
Section titled “ConvexClient surface”These are the members the helpers call. Tests rarely call them directly, but the signatures define what gets captured.
onUpdate( query: unknown, args: Record<string, unknown>, onUpdate: (result: unknown) => unknown, onError?: (err: Error) => unknown,): () => void;
onPaginatedUpdate_experimental( query: unknown, args: Record<string, unknown>, options: { initialNumItems: number }, onUpdate: (result: unknown) => unknown, onError?: (err: Error) => unknown,): () => void;
mutation(fn: unknown, args: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
// No options parameter — this mirrors the real `ConvexClient.action`.action(fn: unknown, args: Record<string, unknown>): Promise<unknown>;
query(fn: unknown, args: Record<string, unknown>): Promise<unknown>;
getAuth(): { token: string; decoded: Record<string, unknown> } | undefined;
connectionState(): ConnectionState;
subscribeToConnectionState(listener: (state: ConnectionState) => void): () => void;
close(): Promise<void>;Behavior worth knowing:
- Both subscription methods return an unsubscribe function that flips the capture’s
unsubscribedflag and increments itsunsubscribeCount. Whendisabledis true the call still succeeds and still returns a callable no-op unsubscribe — the subscription is simply never established, and aMockRefusedSubscriptionis recorded instead of a capture. mutation()andaction()return a promise that only the corresponding capture’sresolve/rejectsettles. Onlymutation()takes an options argument, matching the real client;injectMutation()always passes{ optimisticUpdate }there (the property isundefinedwhen no optimistic update was configured) and it is recorded onMockCallableCall.options. An action’soptionsis therefore alwaysundefined. The mock records the object but never runs an optimistic update — there is no local store for one to write into. Whendisabledis true, both reject withError('ConvexClient is disabled')instead of capturing the call, matching the real client.query()is the one-shot read used by code that injects the client directly, and it follows the real client down both paths. Every call records the lookup onlocalQueryResultCalls. On a warm-cache hit (seeded byseedQueryResult()) it resolves immediately and opens no subscription. On a miss it falls back toonUpdate— so the call appears inquerySubscriptionsand the promise stays pending until that subscription’semit()oremitError()settles it, at which point the read unsubscribes (unsubscribeCountbecomes1). It never resolvesundefinedfor a miss. Whendisabledis true it rejects withError('ConvexClient is disabled')before either path.getAuth()returns whateverseedAuth()last gave the client, orundefined— including whendisabledis true, where the real client has no auth state at all.connectionState()incrementsconnectionStateReadsbefore throwingError('ConvexClient is disabled')whendisabledis true, so a test can prove code under test never even attempted the read.subscribeToConnectionState()incrementsconnectionStateSubscriptionsand returns an unsubscribe that removes just that listener and incrementsconnectionStateUnsubscribes. A disabled client counts the attempt but never registers the listener, so it never unsubscribes either.close()resolves immediately.
Default connection state
Section titled “Default connection state”A fresh MockConvexClient starts connected and idle:
{ hasInflightRequests: false, isWebSocketConnected: true, timeOfOldestInflightRequest: null, hasEverConnected: true, connectionCount: 1, connectionRetries: 0, inflightMutations: 0, inflightActions: 0,}setConnectionState() merges over this, so a partial update leaves the other fields untouched.
Capture types
Section titled “Capture types”MockQuerySubscription
Section titled “MockQuerySubscription”interface MockQuerySubscription { /** The query function reference passed to the helper. */ query: unknown; /** The args the helper subscribed with. */ args: Record<string, unknown>; /** Deliver a result to the subscriber, as the live WebSocket would; a no-op once the helper has unsubscribed. */ emit: (result: unknown) => void; /** Deliver an error to the subscriber; a no-op once the helper has unsubscribed. */ emitError: (err: Error) => void; /** Invoke the retired callback directly, bypassing the unsubscribe gate. The real client cannot do this. */ emitAfterUnsubscribe: (result: unknown) => void; /** The `emitAfterUnsubscribe` counterpart for errors. */ emitErrorAfterUnsubscribe: (err: Error) => void; /** True once the helper has unsubscribed. */ unsubscribed: boolean; /** How many times the helper invoked the unsubscribe function. */ unsubscribeCount: number;}MockPaginatedSubscription
Section titled “MockPaginatedSubscription”interface MockPaginatedSubscription { query: unknown; args: Record<string, unknown>; initialNumItems: number; emit: (result: { results: unknown[]; status: string; loadMore: (n: number) => boolean }) => void; emitError: (err: Error) => void; emitAfterUnsubscribe: (result: { results: unknown[]; status: string; loadMore: (n: number) => boolean }) => void; emitErrorAfterUnsubscribe: (err: Error) => void; unsubscribed: boolean; unsubscribeCount: number;}emit takes the client-shaped paginated result, not the server PaginationResult — see
Driving state.
emit / emitError are gated on unsubscribe, exactly as the real client is, and are what you want
in every ordinary test. emitAfterUnsubscribe / emitErrorAfterUnsubscribe invoke the retired
callback directly, which the real client can never do — see
Emitting after unsubscribe.
MockCallableCall
Section titled “MockCallableCall”Used for both mutations and actions.
interface MockCallableCall { /** The mutation/action function reference. */ fn: unknown; /** The args of the invocation. */ args: Record<string, unknown>; /** The options object the helper passed, if any. `injectMutation` forwards `{ optimisticUpdate }`; `injectAction` passes none. */ options: Record<string, unknown> | undefined; /** Resolve the invocation's promise. */ resolve: (result: unknown) => void; /** Reject the invocation's promise. Accepts a non-Error value so a test can prove the helper normalizes it. */ reject: (err: unknown) => void;}MockAuthRegistration
Section titled “MockAuthRegistration”One entry per client.setAuth call. The real client owns the token lifecycle and calls back into
the code under test; this is the handle for driving that side of the conversation.
interface MockAuthRegistration { /** The token fetcher that was registered. Await it to simulate Convex asking for a token. */ fetchToken: (args: { forceRefreshToken: boolean }) => Promise<string | null | undefined>; /** Report the authentication outcome back. Notification only — it does not give the client a token. */ setAuthenticated: (isAuthenticated: boolean) => void; /** Report a token refresh starting or finishing. A no-op when no `onRefreshChange` callback was registered. */ setRefreshing: (isRefreshing: boolean) => void; /** True once `clearAuth` has run since this registration was made. A recorded fact, not a gate. */ cleared: boolean;}Registering is configuration, not authentication. A registration does not make
client.hasAuth() true and does not populate getAuth(); only seedAuth() does, because the real
hasAuth() reports whether a token is held, not whether a fetcher was installed.
cleared records that clearAuth() ran and nothing more. It does not silence the registration:
the real clearAuth drops the token without tearing down the authentication manager’s
configuration, so a cleared registration can still call back, and this mock lets it. Whatever
ignores that late callback has to be the code under test’s own generation guard.
MockRefusedSubscription
Section titled “MockRefusedSubscription”interface MockRefusedSubscription { /** Which subscription API was reached for. */ kind: 'query' | 'paginated'; /** The query function reference that was passed. */ query: unknown; /** The args the subscription would have used. */ args: Record<string, unknown>;}Only a { disabled: true } mock records these. Nothing is rejected and nothing throws: the
subscribe call succeeds and returns a no-op unsubscribe, as the real disabled client does, and the
subscription is simply never established. Recording it turns “no subscription exists” into positive
evidence about what was asked for and skipped.
MockLocalQueryResultCall
Section titled “MockLocalQueryResultCall”interface MockLocalQueryResultCall { /** The query name that was looked up, e.g. `'todos:list'`. */ queryName: string; /** The args the result was looked up with. */ args: Record<string, unknown> | undefined;}Recorded for both routes into the warm cache: the client.localQueryResult lookups the query
helpers make before their subscription delivers, and the one-shot convex.query() calls made by
code that injects the client directly.
Fault injection
Section titled “Fault injection”Some failures have no knob because they are failures of the client itself, not outcomes it reports —
client.setAuth throwing, client.hasAuth throwing. The mock does not grow an option for each of
them. Spy on the mock’s own client object instead:
jest.spyOn(convex.client, 'setAuth').mockImplementation(() => { throw new Error('sync exploded');});convex.client returns the same object on every access, so the spy stays armed for the code under
test. This is the supported approach — do not hand-roll a replacement ConvexClient fake to
reach one error path, because the replacement stops tracking the mock as it changes and quietly
diverges from what the helpers actually call.
The one prerequisite: convex.client throws while disabled is true, so a disabled mock cannot be
spied on at all.
Limitations
Section titled “Limitations”The mock is deliberately a thin, honest stand-in. Where it does not reproduce the real client, it says so rather than pretending.