개발

jest 세팅 및 zustand 전역 스토어 테스트 구축

Padd60 2023. 11. 1. 00:52

jest 세팅

모듈 다운

사용하는 패키지 매니저에 맞춰 필요한 모듈을 다운받는다.

예시 pnpm

pnpm add -D jest ts-jest @types/jest @types/node ts-loader ts-node babel-jest @babel/core @babel/preset-env @babel/preset-typescript @testing-library/react @testing-library/user-event

config 파일 작성

babel.config.js

module.exports = {
  presets: [
    '@babel/preset-env',
		['@babel/preset-react', {runtime: 'automatic'}],
    '@babel/preset-typescript',
  ],
};

jest.config.ts

import type { JestConfigWithTsJest } from 'ts-jest';

const config: JestConfigWithTsJest = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['./src/setup-jest.ts'],
};

export default config;

setup-jest.ts

import '@testing-library/jest-dom';

package.json

"scripts": {
    "test": "jest"
  }

zustand mock jest 환경 세팅

루트 폴더에 __mocks__ 폴더를 만들고 하위에 zustand.ts 생성

zustand.ts

import { type StateCreator } from 'zustand/vanilla';
import type * as zustand from 'zustand';
import { act } from '@testing-library/react';

const { create: actualCreate, createStore: actualCreateStore } = jest.requireActual<typeof zustand>('zustand');

// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>();

// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = <S>(createState: StateCreator<S>) => {
  console.log('zustand create mock');
  return typeof createState === 'function' ? createInternalFn(createState) : createInternalFn;
};

const createInternalFn = <S>(createState: StateCreator<S>) => {
  const store = actualCreate(createState);
  const initialState = store.getState();
  storeResetFns.add(() => store.setState(initialState, true));
  return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {
  console.log('zustand createStore mock');

  const store = actualCreateStore(stateCreator);
  const initialState = store.getState();
  storeResetFns.add(() => {
    store.setState(initialState, true);
  });
  return store;
}) as typeof zustand.createStore;

// reset all stores after each test run
afterEach(() => {
  act(() => {
    storeResetFns.forEach((resetFn) => {
      resetFn();
    });
  });
});

위와 같이 설정하면 jest 테스트 시 전역스토어를 mock데이터로 넣어주어 작동하게끔 해주어 테스트 코드에서도 zustand의 값을 헨들링 할 수 있음

 

zustand 예제 구축

Counter Store

import { devtools } from 'zustand/middleware';
import { create } from 'zustand';

interface TCountState {
  count: number;
}
interface TCountAction {
  increaseCount: () => void;
  decreaseCount: () => void;
  doubleCount: () => void;
  halfCount: () => void;
}

const countStore = () => ({
  count: 0,
});

export const useCountStore = create(devtools<TCountState>(countStore, { name: 'countStore' }));

export const useCountAction = (): TCountAction => ({
  increaseCount: () => {
    useCountStore.setState((state) => ({ count: state.count + 1 }));
  },
  decreaseCount: () => {
    useCountStore.setState((state) => ({ count: state.count - 1 }));
  },
  doubleCount: () => {
    useCountStore.setState((state) => ({ count: state.count * 2 }));
  },
  halfCount: () => {
    useCountStore.setState((state) => ({ count: Math.floor(state.count / 2) }));
  },
});

Counter UI

import { useCountStore, useCountAction } from '../store/useCountStore';

function Counter() {
  const { count } = useCountStore();
  const { increaseCount, decreaseCount, doubleCount, halfCount } = useCountAction();
  return (
    <div style={{ width: '500px', padding: '10px', border: '1px solid green', margin: '0 auto' }}>
      <h2>Zustand Count Store</h2>
      <div data-testid="count">{count}</div>
      <button onClick={() => increaseCount()}>+</button>
      <button onClick={() => decreaseCount()}>-</button>
      <button onClick={() => doubleCount()}>x2</button>
      <button onClick={() => halfCount()}>/2</button>
    </div>
  );
}

export default Counter;

jest 테스트 코드 작성

import userEvent from '@testing-library/user-event';
import { act, cleanup, render, screen } from '@testing-library/react';
import Counter from '../container/Counter';

afterEach(cleanup);

describe('Counter UI', () => {
	test('카운터 페이지 스냅샷 렌더 테스트', () => {
    const counterPage = render(
      <BrowserRouter>
        <CounterPage />
      </BrowserRouter>,
    );
    expect(counterPage).toMatchSnapshot();
  });

  const findTexts = [
    { id: 'plusBtn', value: '+', type: 'button' },
    { id: 'doubleBtn', value: 'x2', type: 'button' },
    { id: 'halfBtn', value: '/2', type: 'button' },
    { id: 'minusBtn', value: '-', type: 'button' },
  ];

  test('카운트 값 찾고 존재하는지 확인', () => {
    render(<Counter />);

    const countEl = screen.getByTestId('count');

    expect(countEl).toBeTruthy();
  });

  test('카운터 관련 액션 버튼 찾고 존재하는지 확인', () => {
    render(<Counter />);

    findTexts.forEach(({ value }) => {
      expect(screen.getByText(value)).toBeTruthy();
    });
  });

  test('카운터 액션 버튼에 따른 카운터 값 정상반응 확인', async () => {
    render(<Counter />);

    expect(await screen.findByText(0)).toBeTruthy();

    for (const { id, value, type } of findTexts) {
      switch (id) {
        case 'plusBtn':
          clickButton({ type, value });
          expect(await screen.findByText(1)).toBeTruthy();
          break;
        case 'doubleBtn':
          clickButton({ type, value });
          expect(await screen.findByText(2)).toBeTruthy();
          break;
        case 'halfBtn':
          clickButton({ type, value });
          expect(await screen.findByText(1)).toBeTruthy();
          break;
        case 'minusBtn':
          clickButton({ type, value });
          expect(await screen.findByText(0)).toBeTruthy();
          break;
      }
    }
  });
});

const clickButton = async ({ type, value }: { type: string; value: string }) => {
  const user = userEvent.setup();

  await act(async () => {
    await user.click(await screen.findByRole(type, { name: value }));
  });
};

배열 반복문내 비동기 순차적 처리를 위해 for of 문 사용해서 처리 (forEach는 내부 콜백에서 비동기를 순차적으로 처리하지 않음)

 

비고

Getting Started · Jest

 

Getting Started · Jest

Install Jest using your favorite package manager:

jestjs.io

Testing React Apps · Jest

 

Testing React Apps · Jest

At Facebook, we use Jest to test React applications.

jestjs.io

Zustand Documentation

 

Zustand Documentation

Zustand is a small, fast and scalable bearbones state-management solution, it has a comfy api based on hooks

docs.pmnd.rs