Chor
Chor

Reputation: 991

How to test asynchronous state update using zustand and jest?

Say I have a listStore.js like this:

import { create } from 'zustand'
import queryList from '@/service'

const useListStore = () => create((get, set) => ({
   list: [],
   getList: async () => {
      const list = await queryList()
      const { updateList } = get()
      updateList(list)
   }
   updateList: list => {
      set({ 
       list: list.map(i => i + 1)
      })
   }
}))

To test this store hook, I create a test file named listStore.test.js like this:

import useListStore from './useListStore'
import { renderHook, act } from '@testing-library/react'

jest.mock('@/service', () => ({
  queryList: jest.fn().mockResolvedValue([1,2,3])
}))
describe('test listStore', () => {
   test('test updateList function', () => {
      // some codes
   })
   test('test getList function', () => {
      const { result } = renderHook(() => useListStore())
      act(() => result.current.getList())
      expect(queryList).toHaveBeenCalled()
      expect(result.current.list).toEqual([2,3,4])
   })
})

When testing getList function, not only should I test the queryList has been called, but I also need to test that the list state has been updated correctly.

The key point is that, before I do an assertion like expect(result.current.list).toEqual([2,3,4]), I have to make sure that the state update has been done. However, I have no idea how to make sure that. Even when I waitFor or await the expect assertion, the state I get seems to be an old value. The list state is still [] instead of [2,3,4].

Upvotes: 0

Views: 120

Answers (1)

Lin Du
Lin Du

Reputation: 102547

You code has some issues:

  1. The stateCreatorFn passed in create accept set function as its first argument, get function as its second argument.

See function signature below:

export type StateCreator<T, Mis extends [StoreMutatorIdentifier, unknown][] = [], Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T> = ((setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>, getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>, store: Mutate<StoreApi<T>, Mis>) => U) & {
    $$storeMutators?: Mos;
};
  1. create returns a React Hook with API utilities, but you create a high order function. It should be:
const useListStore = create((set, get) => {/** */})

NOT:

const useListStore = () => create((set, get) => {/** */})
  1. You can update the state in the getList action directly by calling set function.
import { create } from 'zustand';
import queryList from './service';

const useListStore = create((set) => ({
  list: [],
  getList: async () => {
    const list = await queryList();
    set({ list: list.map((v) => v + 1) });
  },
}));

export default useListStore;
  1. You maybe use await act(async () => ...), otherwise the test will fail and get the warning
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

Then, the test should be:

useListStore.test.js

import useListStore from './useListStore';
import queryList from './service';
import '@testing-library/jest-dom';
import { renderHook, act } from '@testing-library/react';

jest.mock('./service');

describe('test listStore', () => {
  test('test getList function', async () => {
    queryList.mockResolvedValueOnce([1, 2, 3]);
    const { result } = renderHook(() => useListStore());
    await act(async () => await result.current.getList());
    expect(queryList).toHaveBeenCalled();
    expect(result.current.list).toEqual([2, 3, 4]);
  });
});

service.js:

const queryList = () => ['real data'];

export default queryList;

Test result:

$ npm t -- -o --coverage

> [email protected] test
> jest -o --coverage

 PASS  stackoverflow/79274146/useListStore.test.js                                                                                                                                                                                                                                           
  test listStore
    √ test getList function (12 ms)                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                             
-----------------|---------|----------|---------|---------|-------------------                                                                                                                                                                                                               
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                                                                                                                                                                                                                
-----------------|---------|----------|---------|---------|-------------------
All files        |   85.71 |      100 |      75 |     100 | 
 service.js      |      50 |      100 |       0 |     100 | 
 useListStore.js |     100 |      100 |     100 |     100 | 
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.157 s

package versions:

"@testing-library/react": "^14.1.2",
"jest": "^29.7.0",
"zustand": "^5.0.2",
"react": "^18.2.0",

P.S. I don't have __mocks__/zustand.ts file mentioned here

Upvotes: 0

Related Questions