개발
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는 내부 콜백에서 비동기를 순차적으로 처리하지 않음)
비고