Testing Utilities
Overview
Section titled “Overview”@ic-reactor/core/testing runs the code under test against a fake replica.
The fake answers an HttpAgent’s requests with canisters written as typed
handlers, and signs its answers so the agent verifies them as it would a
replica’s. ClientManager, Reactor, DisplayReactor, their query keys and
error unwrapping, and the React hooks all run unmodified.
It is a separate entry point. The main entry never imports it, so it adds nothing to an app bundle. The Testing guide walks through writing tests with it.
Import
Section titled “Import”import { createTestCanister, installFakeReplica, type FakeReplica,} from "@ic-reactor/core/testing"
// The same bindings, for an app that depends on @ic-reactor/react aloneimport { createTestCanister, installFakeReplica,} from "@ic-reactor/react/testing"installFakeReplica
Section titled “installFakeReplica”function installFakeReplica(options?: FakeReplicaOptions): FakeReplicaReplaces globalThis.fetch with a fake replica and returns it. Install it
before building the agents under test: an HttpAgent keeps the fetch it
found when it was built.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
host |
string |
See below | The origin the fake answers for. Build the agents under test with this host. |
canisters |
Record<string, FakeCanister> |
{} |
The canisters the fake runs, by canister ID. A call to any other canister is rejected. |
A key of canisters that is not a canister ID throws.
With no host, the fake answers where a ClientManager built with no host
sends its calls: the page’s origin when the test environment has a local one
(http://localhost:3000 in Vitest’s jsdom and happy-dom), and
http://127.0.0.1:4943 otherwise, as in plain Node.
Returns: FakeReplica
Section titled “Returns: FakeReplica”| Member | Type | Description |
|---|---|---|
host |
string |
The origin the fake answers for. |
rootKey |
Uint8Array |
The DER root key the fake signs with. An agent on a local host fetches it from the fake. |
requests |
readonly FakeReplicaRequest[] |
Every request an agent sent, in order. |
restore() |
() => void |
Takes the fake out of fetch. Safe to call twice; several fakes restore in any order. |
Each FakeReplicaRequest has an endpoint ("status", "query", "call" or
"read_state"), and where they apply its canisterId, methodName and
caller (as text). A request the fake refused carries a refused reason.
Behaviour
Section titled “Behaviour”| Situation | What the agent gets |
|---|---|
| A query or update call a handler answers | The reply, certified (update) or signed by a node (query), as from a replica |
| A handler throws | A trap’s reject (code 5, IC0503), which a reactor throws as a CallError |
| A call to a canister the fake does not run | A reject with code 3 (DestinationInvalid) |
A canister with no query (or update) handler is called |
A reject with code 3 |
| A request whose signature or delegation does not verify | HTTP 400, recorded with a refused reason |
| An IC API request to another origin | A network error naming the fake’s host, logged once with console.error |
| Any other request, on any origin | Passed to the fetch the fake replaced |
The fake checks signatures by Ed25519, ECDSA P-256 and secp256k1 keys, and delegation chains between them, as a replica does: the signer, each delegation’s signature, expiry and targets. It refuses any other kind of key. The anonymous principal needs no signature.
A call is answered by the canister it names; an effectiveCanisterId only
routes it, as on a replica. Update calls are answered synchronously; nothing
polls. A second fake installed while one is installed answers its own host and
passes the IC API on any other origin to the earlier one.
createTestCanister
Section titled “createTestCanister”function createTestCanister<A = BaseActor>( idlFactory: ReactorParameters["idlFactory"], // what a Reactor takes handlers: TestCanisterHandlers<A>): FakeCanisterBuilds a canister for installFakeReplica from the service’s Candid interface
and a handler per method. The fake decodes each call’s arguments with the
interface, runs the handler and encodes its result.
import { createTestCanister, installFakeReplica,} from "@ic-reactor/core/testing"import { idlFactory, type _SERVICE } from "./declarations/backend"
const replica = installFakeReplica({ canisters: { "bkyz2-fmaaa-aaaaa-qaaaq-cai": createTestCanister<_SERVICE>(idlFactory, { greet: ([name]) => `Hello, ${name}!`, whoami: (_args, { caller }) => caller, transfer: async ([{ amount }]) => amount > 100n ? { Err: { InsufficientFunds: { balance: 100n } } } : { Ok: amount }, }), },})Handlers
Section titled “Handlers”TestCanisterHandlers<A> maps each method name of the service A to an
optional handler:
type TestCanisterHandler<A, M extends FunctionName<A>> = ( args: ActorMethodParameters<A[M]>, context: FakeCallContext) => ActorMethodReturnType<A[M]> | Promise<ActorMethodReturnType<A[M]>>argsis the method’s decoded argument tuple, ascallMethodtakes it.context.calleris thePrincipalthe request was signed as, after the fake checked the signature.- The result is the method’s Candid result: one value, a tuple for several
results, nothing for none. Return
{ Err: ... }to make a reactor throw aCanisterError. - Throwing rejects the call as a trap, and a reactor throws a
CallError.
The values are the raw Candid ones (bigint, Principal, [] | [T] for an
opt) whichever reactor the test calls through.
Rejections
Section titled “Rejections”- A query call to a method the interface does not mark
queryorcomposite_queryis rejected, as a replica rejects it. - A call to a method with no handler is rejected with a message that names the method.
- A result the method’s Candid type does not accept rejects the call with a message that names the handler.
- A handler for a method the service does not have throws when the canister is created.
FakeCanister
Section titled “FakeCanister”installFakeReplica runs any object of this shape. Implement it directly to
answer with bytes the service’s types would not produce.
interface FakeCanister { query?( method: string, arg: Uint8Array, context: FakeCallContext ): Uint8Array | Promise<Uint8Array> update?( method: string, arg: Uint8Array, context: FakeCallContext ): Uint8Array | Promise<Uint8Array>}Each handler receives the Candid-encoded argument and returns the Candid-encoded reply. Throwing rejects the call as a trap.