Using Jest
Jest is a JavaScript testing framework that is widely used in the JavaScript community. It is recommended for beginners because it has the largest community and is the most widely used. Jest is also the officially supported test runner for PicJS.
To get started with Jest, install the relevant packages using your preferred package manager:
npm i -D jest @types/jest @types/node @swc/core @swc/jestpnpm i -D jest @types/jest @types/node @swc/core @swc/jestbun add -d jest @types/jest @types/node @swc/core @swc/jestyarn add -D jest @types/jest @types/node @swc/core @swc/jestCreate a tsconfig.json file:
{ "compilerOptions": { // enable latest features "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "allowJs": true, // allow importing `.js` from `.ts` "types": ["jest", "node"],
// Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true,
// Best practices "strict": true, "skipLibCheck": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true,
// Some stricter flags "useUnknownInCatchVariables": true, "noPropertyAccessFromIndexSignature": true }, "include": [ "./tests/**/*.ts", "./global-setup.ts", "./global-teardown.ts", "./types.d.ts" ]}Create a jest.config.ts file:
import type { Config } from 'jest';
const config: Config = { watch: false, transform: { '^.+\\.(t|j)sx?$': '@swc/jest', }, transformIgnorePatterns: [ 'node_modules/(?!.*(@noble|@scure))', '\\.pnp\\.[^\\\\/]+$', ], testEnvironment: 'node', globalSetup: '<rootDir>/global-setup.ts', globalTeardown: '<rootDir>/global-teardown.ts', testTimeout: 30_000,};
export default config;The transformIgnorePatterns entry is required. PicJS depends on @icp-sdk/core, which in turn depends on @noble/hashes and @noble/curves. Those packages ship ES modules only, and Jest resolves the CommonJS build of @icp-sdk/core, so they have to be transformed instead of ignored along with the rest of node_modules. This also requires a transform that handles .js files, such as the one above. Without it, tests fail to load with one of these errors, depending on the Jest version:
SyntaxError: Cannot use import statement outside a moduleMust use import to load ES Module: .../node_modules/@noble/hashes/sha2.jsThe pattern matches these packages at any depth, since npm and Yarn may nest them under a dependent. The second entry repeats Jest’s default, which a custom transformIgnorePatterns replaces rather than extends. Vitest loads ES modules natively and needs no equivalent setting.
You can also check out the official the @swc/jest documentation for more information on configuring this file.
Then, add a test script to your package.json:
{ "scripts": { "test": "jest" }}The PocketIC server needs to be started before running tests and stopped once they’re finished running. This can be done by creating global-setup.ts and global-teardown.ts files in your project’s root directory:
import { PocketIcServer } from '@dfinity/pic';
module.exports = async function (): Promise<void> { const pic = await PocketIcServer.start(); const url = pic.getUrl();
process.env.PIC_URL = url; global.__PIC__ = pic;};module.exports = async function () { await global.__PIC__.stop();};To improve type-safety for process.env.PIC_URL and global.__PIC__, create a types.d.ts file:
import { PocketIcServer } from '@dfinity/pic';
declare global { declare var __PIC__: PocketIcServer;
namespace NodeJS { interface ProcessEnv { PIC_URL: string; } }}Writing tests
Section titled “Writing tests”Jest tests are very similar to tests written with Jasmine, or Vitest so they will feel very familiar to developers who have used these frameworks before.
The basic skeleton of all PicJS tests written with Jest will look something like this:
import { resolve } from 'node:path';import { type Actor, PocketIc } from '@dfinity/pic';
// Import the declarations generated for your canister,// see the Canister declarations guideimport { idlFactory, type _SERVICE } from './declarations/backend.did';
// Define the path to your canister's WASM file.// icp-cli writes it to `.icp/cache/artifacts/<canister name>`.const WASM_PATH = resolve( __dirname, '..', '.icp', 'cache', 'artifacts', 'backend',);
// The `describe` function is used to group tests together// and is completely optional.describe('backend', () => { // Define variables to hold our PocketIC instance // and an actor to interact with our canister. let pic: PocketIc; let actor: Actor<_SERVICE>;
// The `beforeEach` hook runs before each test. // // This can be replaced with a `beforeAll` hook to persist canister // state between tests. beforeEach(async () => { // create a new PocketIC instance pic = await PocketIc.create(process.env.PIC_URL);
// Setup the canister and actor const fixture = await pic.setupCanister<_SERVICE>({ idlFactory, wasm: WASM_PATH, });
// Save the actor for use in tests actor = fixture.actor; });
// The `afterEach` hook runs after each test. // // This should be replaced with an `afterAll` hook if you use // a `beforeAll` hook instead of a `beforeEach` hook. afterEach(async () => { // tear down the PocketIC instance await pic.tearDown(); });
// The `it` function is used to define individual tests it('should greet', async () => { const response = await actor.greet('PicJS');
expect(response).toEqual('Hello, PicJS!'); });});You can also check out the official Jest getting started documentation for more information on writing tests.