createTestClient
createTestClient(
options?):object
Defined in: core/src/testing/test-client.ts:251
Creates a client for a test: a real one, over a fake replica, signed in as a principal the test picks and can change.
const { client, auth, mock } = createTestClient()mock<Actor>(actor, LEDGER, { icrc1_balance_of: ({ owner }) => (owner === auth.getPrincipal()?.toText() ? 7n : 0n),})const ledger = client.canister<Actor>(actor, { id: LEDGER })await ledger.icrc1_balance_of({ owner: me, subaccount: null }) // 7nIt runs the same code as production, so a test sees what an app would: a
call is signed by the principal signed in when it is made, an update by
nobody is refused before it is sent, a reply that is lost is
outcome_unknown. Each call is also checked as a replica checks it: a
canister’s ctx.caller is the principal the fake verified, never a guess.
The fake replica signs with @noble/curves, an optional peer dependency of
@ic-reactor/core that @icp-sdk/core already installs. Add it to your
devDependencies if your package manager does not let this package resolve
it. Nothing global is replaced, globalThis.fetch included, so two test
clients run side by side, each with its own fake replica and sign-in.
Parameters
Section titled “Parameters”options?
Section titled “options?”Who is signed in, and optionally which network the client
believes it is on. Alone, it signs in as the identity of seed 1.
identity?
Section titled “identity?”number | Identity
Who is signed in at first: an identity, which must be a real signing
identity such as Ed25519KeyIdentity, or a number that stands for one
(the same number is the same principal in every run, on every machine).
Default Value
The identity of seed 1.
signedIn?
Section titled “signedIn?”boolean
Whether the client starts signed in. When false it starts signed out,
calls as the anonymous principal (every update is refused), and
auth.signIn() signs in as identity.
Default Value
truenetwork?
Section titled “network?”Which network the client believes it is on, for the parts a test can see:
the network segment of its query keys and the host it connects to. The
fake replica answers on that host. The root key is always the fake’s own:
a rootKey or fetchRootKey in a network object is ignored, and "ic"
does not check certificates against mainnet’s key, which the fake cannot
sign with. Give the host with a scheme.
Default Value
A replica on the fake's own host.allowEnvConfig?
Section titled “allowEnvConfig?”boolean
Whether the ic_env cookie is believed, as for createClient. It
matters only to a test that resolves { name } canisters from a cookie
it set up, and a { name } canister resolves only on a page (in jsdom
or happy-dom), never in Node, which is where a test client runs by
default: a Node test names its canisters with { id }. The root key is
never taken from the cookie: it is the fake’s own.
maxDepth?
Section titled “maxDepth?”number
How deep a decoded value may nest, for the client and for the mocks. Defaults to 256.
Returns
Section titled “Returns”The client, its sign-in, the means to mock canisters and to fail a call on purpose, and the log of the requests the fake received.
client
Section titled “client”
readonlyclient:Client
A real client (createClient’s) whose agents send through the fake
replica, and whose auth is auth. Unlike a client made
by createClient, its auth is built in every environment, a server one
such as Node included, so signing in and out works in any test runner.
Dispose it at the end of the test, as any client.
readonlyauth:object
The sign-in the client calls as: signIn, signOut, switchTo another
identity or seed, expire the session, or make it elsewhere. The status
changes at once, and the client’s next call is signed by whoever is
signed in then. The client owns it once anything has asked it who the
caller is: a call, caller(), authState(), subscribe(), signIn(),
signOut(), and also queryKey() and queryOptions(), which key by the
caller (a mutation from mutationOptions() reads it when it runs, as a
call does). It disposes it with itself then. A client disposed before any
of those never touched auth and leaves it alone, so a test can borrow
auth from a client it never used and hand it to another one.
auth.getPrincipal()
Section titled “auth.getPrincipal()”getPrincipal():
Principal|undefined
The principal calls are accepted as, or undefined unless the status is
signed-in: an expired or elsewhere session names a principal in
getStatus() but cannot act. Synchronous, and derived from
the same state as getStatus(), so the two never disagree.
Returns
Section titled “Returns”Principal | undefined
auth.getStatus()
Section titled “auth.getStatus()”getStatus(): {
state:"signed-in";principal:Principal;expiresAtMs:number; } | {state:"expired";principal:Principal;expiresAtMs:number; } | {state:"signed-in-elsewhere";principal:Principal;expiresAtMs:number; } | {state:"signed-out"; }
Who is signed in, as AuthClient.getStatus() reports it: its state is
signed-in (calls as principal are accepted), expired (the session of
principal has ended), signed-in-elsewhere (principal is signed in on
this domain, but this origin holds no credential for it, so it cannot act
yet) or signed-out (nobody is, and there is no principal).
Synchronous. The object it returns is the same one until the status
changes, so it is safe to read from useSyncExternalStore.
Returns
Section titled “Returns”{ state: "signed-in"; principal: Principal; expiresAtMs: number; } | { state: "expired"; principal: Principal; expiresAtMs: number; } | { state: "signed-in-elsewhere"; principal: Principal; expiresAtMs: number; } | { state: "signed-out"; }
auth.getIdentity()
Section titled “auth.getIdentity()”getIdentity():
Promise<Identity>
The identity to sign calls with: the signed-in one, or
AnonymousIdentity in every other state, as AuthClient answers while
nobody is signed in.
Returns
Section titled “Returns”Promise<Identity>
auth.subscribe()
Section titled “auth.subscribe()”subscribe(
listener): () =>void
Calls listener after each change of who is signed in, once per change,
with no argument: read the answer from getStatus().
Parameters
Section titled “Parameters”listener
Section titled “listener”() => void
Returns
Section titled “Returns”A function that stops listening.
() => void
auth.signIn()
Section titled “auth.signIn()”signIn(
identityOrSeed?):Promise<Identity>
Signs in as identityOrSeed, or as the identity the auth last signed in
as when it is left out. The status changes before the promise is
returned, so a test need not await it, and listeners are told once.
An argument that is neither a seed nor an identity, such as the options
object an AuthClient takes ({ maxTimeToLive }), is ignored: the auth
signs in as the identity it last signed in as, so a client that forwards
its own options does not change who signs in.
Parameters
Section titled “Parameters”identityOrSeed?
Section titled “identityOrSeed?”number | Identity
Returns
Section titled “Returns”Promise<Identity>
auth.signOut()
Section titled “auth.signOut()”signOut():
Promise<void>
Signs out. Listeners are told once, unless nobody was signed in. The
auth remembers the identity, so a later signIn() returns to it.
Returns
Section titled “Returns”Promise<void>
auth.switchTo()
Section titled “auth.switchTo()”switchTo(
identityOrSeed):Identity
Signs in as another identity in one step: listeners are told once and never see a signed-out status in between, as when a user picks another account in the identity provider.
Parameters
Section titled “Parameters”identityOrSeed
Section titled “identityOrSeed”number | Identity
Returns
Section titled “Returns”Identity
The identity now signed in.
Throws
Section titled “Throws”When identityOrSeed is neither a seed nor an
identity: unlike signIn(), switching has no account to fall
back to.
auth.expire()
Section titled “auth.expire()”expire():
void
Ends the session of the identity the auth last signed in as, in another
tab or by running out its time, so the status becomes expired.
Listeners are told once, unless it already was.
Returns
Section titled “Returns”void
auth.elsewhere()
Section titled “auth.elsewhere()”elsewhere():
void
Makes the identity the auth last signed in as signed in on this domain
but not held by this origin, so the status becomes signed-in-elsewhere.
Listeners are told once, unless it already was.
Returns
Section titled “Returns”void
auth.dispose()
Section titled “auth.dispose()”dispose():
void
Releases every listener. After it the auth tells nobody, and a
subscribe() listens to nothing.
Returns
Section titled “Returns”void
auth.disposed
Section titled “auth.disposed”
readonlydisposed:boolean
Whether dispose() has been called.
auth.listenerCount
Section titled “auth.listenerCount”
readonlylistenerCount:number
How many listeners are subscribed.
mock()
Section titled “mock()”mock<
A>(service,id,handlers):void
Runs a canister at id, answering with handlers, replacing any canister
already there. id is a canister id, or aaaaa-aa for the management
canister.
service is the actor export of the generated module and A its
Actor type, as for client.canister<A>(service, { id }): write
mock<Actor>(actor, id, handlers). Methods answer by their mode: a
query method on the query path, an update on the call path (a query
method also answers a replicated call, as it does on a canister, which is
how a certified canister reads it). A call to a method with no handler
traps, with a message that names the method.
What goes wrong in a handler goes wrong as it does in a canister: a
handler that calls reject() rejects the call with that
reject code, and one that throws anything else traps it (reject code 5,
with the error’s message). So does a call whose arguments do not decode
with service, and a handler that returns something the method’s result
does not encode.
Type Parameters
Section titled “Type Parameters”A
Parameters
Section titled “Parameters”service
Section titled “service”Schema<Principal>
string
handlers
Section titled “handlers”TestHandlers<A>
Returns
Section titled “Returns”void
Throws
Section titled “Throws”TypeError for a service that is not a service schema, an id
that is not a canister id, a handler that is not a function, and a handler
for a method the service does not have.
reject()
Section titled “reject()”reject(
code,message?):never
Rejects the call being handled with code, as a canister does: call it
from inside a handler. The reject codes are the interface specification’s:
1 SYS_FATAL, 2 SYS_TRANSIENT, 3 DESTINATION_INVALID, 4
CANISTER_REJECT, 5 CANISTER_ERROR and 6 SYS_UNKNOWN. An update is
rejected in a certificate and a query in the signed query response.
Parameters
Section titled “Parameters”1 | 2 | 3 | 4 | 5 | 6
The reject code the client receives.
message?
Section titled “message?”string
The reject message; a default naming the code is used when it is left out.
Returns
Section titled “Returns”never
dropNextReply()
Section titled “dropNextReply()”dropNextReply():
void
Loses the reply to the next update call: the canister runs, but the client
sees a network failure instead of the answer, so the call ends as
outcome_unknown with mayHaveExecuted: true. The same request is never
run twice: the fake keeps its reply lost whenever it is asked again.
Returns
Section titled “Returns”void
refuseNext()
Section titled “refuseNext()”Call Signature
Section titled “Call Signature”refuseNext(
status,options):void
Answers the next times canister requests that name method and are
addressed to canister (each one that is given) with an HTTP error
status before any canister sees them, and lets every other request
through: refuseNext(429, { method: "icrc1_transfer" }) throttles the
transfer while the reads around it are answered.
A query and a replicated call of the same method both match. canister
is the canister the request names (aaaaa-aa for a call to the
management canister), and need not be mocked: a gateway refuses a request
for a canister that does not exist. times counts matching requests,
each send of a call the client re-sends included. A request that several
refusals match is refused by the one armed first.
Parameters
Section titled “Parameters”status
Section titled “status”number
An HTTP error status, from 400 to 599.
options
Section titled “options”Which requests to refuse, and how many (times,
default 1). Options with neither method nor canister refuse whatever
query or call comes next, as a count does.
method?
Section titled “method?”string
canister?
Section titled “canister?”string
times?
Section titled “times?”number
Returns
Section titled “Returns”void
Throws
Section titled “Throws”RangeError for a status that is not an HTTP error status, or a
times that is not a count of at least 1; TypeError for an option that
does not exist, an empty method, or a canister that is not a canister
id.
Call Signature
Section titled “Call Signature”refuseNext(
status,times?):void
Answers the next times canister requests (a query or a call) with an
HTTP error status before any canister sees them, as a gateway that
throttles does: refuseNext(429) is what a client is told when it is
rate limited. The status and read_state requests an agent makes on its
own are never refused, so the refusal waits for the next query or call,
whatever its method or canister.
Parameters
Section titled “Parameters”status
Section titled “status”number
An HTTP error status, from 400 to 599.
times?
Section titled “times?”number
How many requests to refuse.
Returns
Section titled “Returns”void
Default Value
Section titled “Default Value”1requests
Section titled “requests”
readonlyrequests: readonlyFakeReplicaRequest[]
Every request the client sent the fake replica, in order, as the fake saw it. The same array grows; copy it to keep a moment of it.
An entry has an endpoint ("status", "query", "call" or
"read_state") and, where they apply:
canisterId: the canister the request was addressed to, as text.effectiveCanisterId: the canister it was routed by. It differs fromcanisterIdfor a call to the management canister (aaaaa-aa), where the client takes it from the call’s arguments.methodName: the method aqueryorcallnamed.caller: the principal the request came from, as text, after the fake checked its signature. A canister’sctx.calleris this.refused: why the fake answered before any canister saw the request: a signature, delegation or target that did not check out, as a replica refuses it, or arefuseNext()status.dropped:truewhendropNextReply()lost the reply to it.
Throws
Section titled “Throws”TypeError for an option that does not exist, or one that cannot be used.
Example
Section titled “Example”import { createTestClient } from "@ic-reactor/core/testing"import { actor, type Actor } from "./canisters/icrc1"
const { client, auth, mock, reject } = createTestClient({ identity: 1 })mock<Actor>(actor, LEDGER, { icrc1_transfer: (_arg, { caller }) => caller === BROKE ? reject(4, "no funds") : { tag: "Ok", value: 1n },})const ledger = client.canister<Actor>(actor, { id: LEDGER })
await ledger.icrc1_transfer(arg) // 1n, as the identity of seed 1auth.switchTo(2) // the next call is signed by another principal