Testing Jazz Apps
As you develop your Jazz app, you might find yourself needing to test functionality relating to sync, identities, and offline behaviour. The jazz-tools/testing utilities provide helpers to enable you to do so.
Core test helpers
Jazz provides some key helpers that you can use to simplify writing complex tests for your app's functionality.
setupJazzTestSync
This should normally be the first thing you call in your test setup, for example in a beforeEach or beforeAll block. This function sets up an in-memory sync node for the test session, which is needed in case you want to test data synchronisation functionality. Test data is not persisted, and no clean-up is needed between test runs.
describe<object>(name: string | Function, fn?: SuiteFactory<object> | undefined, options?: number | TestOptions): SuiteCollector<object> (+2 overloads)Creates a suite of tests, allowing for grouping and hierarchical organization of tests. Suites can contain both tests and other suites, enabling complex test structures.describe("My app's tests", () => {beforeEach<object>(fn: BeforeEachListener<object>, timeout?: number): voidRegisters a callback function to be executed before each test within the current suite. This hook is useful for scenarios where you need to reset or reinitialize the test environment before each test runs, such as resetting database states, clearing caches, or reinitializing variables. **Note:** The `beforeEach` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file.beforeEach(async () => { awaitsetupJazzTestSync(); });function setupJazzTestSync({ asyncPeers, }?: { asyncPeers?: boolean; }): Promise<Account>test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestCollectorOptions): void (+2 overloads)Defines a test case with a given name and test function. The test function can optionally be configured with test options.test("I can create a test account", async () => { // See below for details on createJazzTestAccount() constaccount1 = awaitconst account1: { readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & AccountcreateJazzTestAccount({createJazzTestAccount<co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Account | Group>; }>>(options?: { isCurrentActiveAccount?: boolean; AccountSchema?: co.Account<...> | undefined; creationProps?: Record<string, unknown>; } | undefined): Promise<...>AccountSchema:AccountSchema?: co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Account | Group>; }> | undefinedMyAccountSchema,const MyAccountSchema: co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Account | Group>; }>isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true });expect(expect<{ readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & Account>(actual: { ...; } & Account, message?: string): Assertion<...> (+1 overload)account1).const account1: { readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & Accountnot.not: Assertion<{ readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & Account>JestAssertion<{ readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & Account>.toBeUndefined: () => voidUsed to check that a variable is undefined.toBeUndefined(); // ... }); });
createJazzTestAccount
After you've created the initial account using setupJazzTestSync, you'll typically want to create user accounts for running your tests.
You can use createJazzTestAccount() to create an account and link it to the sync node. By default, this account will become the currently active account (effectively the 'logged in' account).
You can use it like this:
constaccount = awaitconst account: { readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & AccountcreateJazzTestAccount({createJazzTestAccount<co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Group | Account>; }>>(options?: { isCurrentActiveAccount?: boolean; AccountSchema?: co.Account<...> | undefined; creationProps?: Record<string, unknown>; } | undefined): Promise<...>AccountSchema:AccountSchema?: co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Group | Account>; }> | undefinedMyAccountSchema,const MyAccountSchema: co.Account<{ profile: co.Profile<BaseProfileShape>; root: co.Map<{}, unknown, Group | Account>; }>isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true,creationProps?: Record<string, unknown> | undefinedcreationProps: {}, });
AccountSchema
This option allows you to provide a custom account schema to the utility to be used when creating the account. The account will be created based on the schema, and all attached migrations will run.
isCurrentActiveAccount
This option (disabled by default) allows you to quickly switch to the newly created account when it is created.
constaccount1 = awaitconst account1: Account | ({ readonly [x: string]: any; } & Account)createJazzTestAccount({createJazzTestAccount<(CoValueClass<Account> & { fromNode: (typeof Account)["fromNode"]; } & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: { ...; } | undefined): Promise<...>isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, }); constconst group1: Groupgroup1 =import coco.group().function group(): co.Group export groupcreate(); // Group is owned by account1; constGroupSchema.create(options?: { owner: Account; } | Account): Groupaccount2 = awaitconst account2: Account | ({ readonly [x: string]: any; } & Account)createJazzTestAccount(); constcreateJazzTestAccount<(CoValueClass<Account> & { fromNode: (typeof Account)["fromNode"]; } & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: { ...; } | undefined): Promise<...>const group2: Groupgroup2 =import coco.group().function group(): co.Group export groupcreate(); // Group is still owned by account1;GroupSchema.create(options?: { owner: Account; } | Account): Group
creationProps
This option allows you to specify creationProps for the account which are used during the account creation (and passed to the migration function on creation).
Managing active Accounts
During your tests, you may need to manage the currently active account after account creation, or you may want to simulate behaviour where there is no currently active account.
setActiveAccount
Use setActiveAccount() to switch between active accounts during a test run.
You can use this to test your app with multiple accounts.
constaccount1 = awaitconst account1: Account | ({ readonly [x: string]: any; } & Account)createJazzTestAccount({createJazzTestAccount<(CoValueClass<Account> & { fromNode: (typeof Account)["fromNode"]; } & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: { ...; } | undefined): Promise<...>isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, }); constaccount2 = awaitconst account2: Account | ({ readonly [x: string]: any; } & Account)createJazzTestAccount(); constcreateJazzTestAccount<(CoValueClass<Account> & { fromNode: (typeof Account)["fromNode"]; } & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: { ...; } | undefined): Promise<...>const group1: Groupgroup1 =import coco.group().function group(): co.Group export groupcreate(); // Group is owned by account1;GroupSchema.create(options?: { owner: Account; } | Account): Groupconst group1: Groupgroup1.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(account2, 'reader'); constconst account2: Account | ({ readonly [x: string]: any; } & Account)myMap =const myMap: { readonly text: string; } & CoMapMyMap.const MyMap: co.Map<{ text: z.z.ZodString; }, unknown, Account | Group>create({CoMapSchema<{ text: ZodString; }, unknown, Account | Group>.create(init: { text: string; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)text: stringtext: "Created by account1" }, {owner?: Group | undefinedowner:const group1: Groupgroup1 }); constconst myMapId: stringmyMapId =myMap.const myMap: { readonly text: string; } & CoMapCoMap.$jazz: CoMapJazzApi<{ readonly text: string; } & CoMap>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoValueJazzApi<V extends CoValue>.id: stringid;function setActiveAccount(account: Account): voidsetActiveAccount(account2); // myMap is still loaded as account1, so we need to load again as account2 constconst account2: Account | ({ readonly [x: string]: any; } & Account)myMapFromAccount2 = awaitconst myMapFromAccount2: ({ readonly text: string; } & CoMap) | nullMyMap.const MyMap: co.Map<{ text: z.z.ZodString; }, unknown, Account | Group>load(CoMapSchema<{ text: ZodString; }, unknown, Account | Group>.load<true>(id: string, options?: { resolve?: RefsToResolve<{ readonly text: string; } & CoMap, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; skipRetry?: boolean; unstable_branch?: BranchDefinition; } | undefined): Promise<...>const myMapId: stringmyMapId);expect(function expect(actual?: any): { toBe(expected?: any): null; toThrow(): null; }myMapFromAccount2?.const myMapFromAccount2: ({ readonly text: string; } & CoMap) | nulltext: string | undefinedtext) .function toBe(expected?: any): nulltoBe('Created by account1');expect(() =>function expect(actual?: any): { toBe(expected?: any): null; toThrow(): null; }myMapFromAccount2?.const myMapFromAccount2: ({ readonly text: string; } & CoMap) | nullCoMap.$jazz: CoMapJazzApi<{ readonly text: string; } & CoMap>Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<{ readonly text: string; } & CoMap>.set<K>(key: K, value: CoFieldInit<({ readonly text: string; } & CoMap)[K]>): voidSet a value on the CoMapset('text', 'Updated by account2')) .function toThrow(): nulltoThrow();
runWithoutActiveAccount
If you need to test how a particular piece of code behaves when run without an active account.
constaccount1 = awaitconst account1: Account | ({ readonly [x: string]: any; } & Account)createJazzTestAccount({createJazzTestAccount<(CoValueClass<Account> & { fromNode: (typeof Account)["fromNode"]; } & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: { ...; } | undefined): Promise<...>isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, });runWithoutActiveAccount<void>(callback: () => void): voidRun a callback without an active account. Takes care of restoring the active account after the callback is run. If the callback returns a promise, waits for it before restoring the active account.runWithoutActiveAccount(() => {expect(() =>function expect(actual?: any): { toBe(expected?: any): null; toThrow(): null; }import coco.group().function group(): co.Group export groupcreate()).GroupSchema.create(options?: { owner: Account; } | Account): Groupfunction toThrow(): nulltoThrow(); // can't create new group });
Managing Context
To test UI components, you may need to create a mock Jazz context.
In most cases, you'd use this for initialising a provider. You can see how we initialise a test provider for React tests here, or see how you could integrate with @testing-library/react here.
Simulating connection state changes
You can use MockConnectionStatus.setIsConnected(isConnected: boolean) to simulate disconnected and connected states (depending on whether isConnected is set to true or false).
Next Steps
You're ready to start writing your own tests for your Jazz apps now. For further details and reference, you can check how we do our testing below.