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.
@paramname - The name of the suite, used for identification and reporting.@paramfn - A function that defines the tests and suites within this suite.@example```ts // Define a suite with two tests describe('Math operations', () => { test('should add two numbers', () => { expect(add(1, 2)).toBe(3); }); test('should subtract two numbers', () => { expect(subtract(5, 2)).toBe(3); }); }); ```@example```ts // Define nested suites describe('String operations', () => { describe('Trimming', () => { test('should trim whitespace from start and end', () => { expect(' hello '.trim()).toBe('hello'); }); }); describe('Concatenation', () => { test('should concatenate two strings', () => { expect('hello' + ' ' + 'world').toBe('hello world'); }); }); }); ```
describe
("My app's tests", () => {
beforeEach<object>(fn: BeforeEachListener<object>, timeout?: number): void
Registers 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.
@paramfn - The callback function to be executed before each test. This function receives an `TestContext` parameter if additional test context is needed.@paramtimeout - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.@returns@example```ts // Example of using beforeEach to reset a database state beforeEach(async () => { await database.reset(); }); ```
beforeEach
(async () => {
await
function setupJazzTestSync({ asyncPeers, }?: {
    asyncPeers?: boolean;
}): Promise<Account>
setupJazzTestSync
();
}); 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.
@paramname - The name of the test or a function that will be used as a test name.@paramoptionsOrFn - Optional. The test options or the test function if no explicit name is provided.@paramoptionsOrTest - Optional. The test function or options, depending on the previous parameters.@throws{Error} If called inside another test function.@example```ts // Define a simple test test('should add two numbers', () => { expect(add(1, 2)).toBe(3); }); ```@example```ts // Define a test with options test('should subtract two numbers', { retry: 3 }, () => { expect(subtract(5, 2)).toBe(3); }); ```
test
("I can create a test account", async () => {
// See below for details on createJazzTestAccount() const
const account1: {
    readonly profile: {
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap;
    readonly root: {} & CoMap;
} & Account
account1
= await
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<...>
createJazzTestAccount
({
AccountSchema?: co.Account<{
    profile: co.Profile<BaseProfileShape>;
    root: co.Map<{}, unknown, Account | Group>;
}> | undefined
AccountSchema
:
const MyAccountSchema: co.Account<{
    profile: co.Profile<BaseProfileShape>;
    root: co.Map<{}, unknown, Account | Group>;
}>
MyAccountSchema
,
isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true });
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)
expect
(
const account1: {
    readonly profile: {
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap;
    readonly root: {} & CoMap;
} & Account
account1
).
not: Assertion<{
    readonly profile: {
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap;
    readonly root: {} & CoMap;
} & Account>
not
.JestAssertion<{ readonly profile: { readonly name: string; readonly inbox: string | undefined; readonly inboxInvite: string | undefined; } & CoMap; readonly root: {} & CoMap; } & Account>.toBeUndefined: () => void
Used to check that a variable is undefined.
@exampleexpect(value).toBeUndefined();
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:

const 
const account: {
    readonly profile: {
        readonly name: string;
        readonly inbox: string | undefined;
        readonly inboxInvite: string | undefined;
    } & CoMap;
    readonly root: {} & CoMap;
} & Account
account
= await
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<...>
createJazzTestAccount
({
AccountSchema?: co.Account<{
    profile: co.Profile<BaseProfileShape>;
    root: co.Map<{}, unknown, Group | Account>;
}> | undefined
AccountSchema
:
const MyAccountSchema: co.Account<{
    profile: co.Profile<BaseProfileShape>;
    root: co.Map<{}, unknown, Group | Account>;
}>
MyAccountSchema
,
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.

const 
const account1: Account | ({
    readonly [x: string]: any;
} & Account)
account1
= await
createJazzTestAccount<(CoValueClass<Account> & {
    fromNode: (typeof Account)["fromNode"];
} & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: {
    ...;
} | undefined): Promise<...>
createJazzTestAccount
({
isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, }); const const group1: Groupgroup1 = import coco.
function group(): co.Group
export group
group
().
GroupSchema.create(options?: {
    owner: Account;
} | Account): Group
create
(); // Group is owned by account1;
const
const account2: Account | ({
    readonly [x: string]: any;
} & Account)
account2
= await
createJazzTestAccount<(CoValueClass<Account> & {
    fromNode: (typeof Account)["fromNode"];
} & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: {
    ...;
} | undefined): Promise<...>
createJazzTestAccount
();
const const group2: Groupgroup2 = import coco.
function group(): co.Group
export group
group
().
GroupSchema.create(options?: {
    owner: Account;
} | Account): Group
create
(); // Group is still owned by account1;

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.

const 
const account1: Account | ({
    readonly [x: string]: any;
} & Account)
account1
= await
createJazzTestAccount<(CoValueClass<Account> & {
    fromNode: (typeof Account)["fromNode"];
} & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: {
    ...;
} | undefined): Promise<...>
createJazzTestAccount
({
isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, }); const
const account2: Account | ({
    readonly [x: string]: any;
} & Account)
account2
= await
createJazzTestAccount<(CoValueClass<Account> & {
    fromNode: (typeof Account)["fromNode"];
} & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: {
    ...;
} | undefined): Promise<...>
createJazzTestAccount
();
const const group1: Groupgroup1 = import coco.
function group(): co.Group
export group
group
().
GroupSchema.create(options?: {
    owner: Account;
} | Account): Group
create
(); // Group is owned by account1;
const group1: Groupgroup1.Group.addMember(member: Account, role: AccountRole): void (+3 overloads)addMember(
const account2: Account | ({
    readonly [x: string]: any;
} & Account)
account2
, 'reader');
const
const myMap: {
    readonly text: string;
} & CoMap
myMap
=
const MyMap: co.Map<{
    text: z.z.ZodString;
}, unknown, Account | Group>
MyMap
.
CoMapSchema<{ text: ZodString; }, unknown, Account | Group>.create(init: {
    text: string;
}, options?: {
    owner?: Group;
    unique?: CoValueUniqueness["uniqueness"];
} | Group): {
    ...;
} & CoMap (+1 overload)
create
({
text: stringtext: "Created by account1" }, { owner?: Group | undefinedowner: const group1: Groupgroup1 }); const const myMapId: stringmyMapId =
const myMap: {
    readonly text: string;
} & CoMap
myMap
.
CoMap.$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(
const account2: Account | ({
    readonly [x: string]: any;
} & Account)
account2
);
// myMap is still loaded as account1, so we need to load again as account2 const
const myMapFromAccount2: ({
    readonly text: string;
} & CoMap) | null
myMapFromAccount2
= await
const MyMap: co.Map<{
    text: z.z.ZodString;
}, unknown, Account | Group>
MyMap
.
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<...>
load
(const myMapId: stringmyMapId);
function expect(actual?: any): {
    toBe(expected?: any): null;
    toThrow(): null;
}
expect
(
const myMapFromAccount2: ({
    readonly text: string;
} & CoMap) | null
myMapFromAccount2
?.text: string | undefinedtext)
.function toBe(expected?: any): nulltoBe('Created by account1');
function expect(actual?: any): {
    toBe(expected?: any): null;
    toThrow(): null;
}
expect
(() =>
const myMapFromAccount2: ({
    readonly text: string;
} & CoMap) | null
myMapFromAccount2
?.
CoMap.$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]>): void
Set a value on the CoMap
@paramkey The key to set@paramvalue The value to set@categoryContent
set
('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.

const 
const account1: Account | ({
    readonly [x: string]: any;
} & Account)
account1
= await
createJazzTestAccount<(CoValueClass<Account> & {
    fromNode: (typeof Account)["fromNode"];
} & CoValueFromRaw<Account>) | CoreAccountSchema<...>>(options?: {
    ...;
} | undefined): Promise<...>
createJazzTestAccount
({
isCurrentActiveAccount?: boolean | undefinedisCurrentActiveAccount: true, }); runWithoutActiveAccount<void>(callback: () => void): void
Run 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.
@paramcallback - The callback to run.@returnsThe result of the callback.
runWithoutActiveAccount
(() => {
function expect(actual?: any): {
    toBe(expected?: any): null;
    toThrow(): null;
}
expect
(() => import coco.
function group(): co.Group
export group
group
().
GroupSchema.create(options?: {
    owner: Account;
} | Account): Group
create
()).function 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.