React Testing: Reducer testing (with Redux)
๐ง Redux๋ ํ ์คํธํ๊ธฐ ์ฝ๋ค?
๋ฆฌ์กํธ๋ก ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์,
์ ์ญ์ํ๊ด๋ฆฌ๋ฅผ ์ํด ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํ๋ ์ด์ ์ค ํ๋๋ ํ
์คํธํ๊ธฐ ์ฝ๋ค๋ผ๋ ๊ฒ์ด๋ค.
ํ
์คํธ์ฝ๋๋ฅผ ์ง์ ์์ฑํด๋ณด๊ธฐ ์ ์๋ ์ด๋ค ์ ์์ ์ฝ๋ค๋ ๊ฑด์ง ์ ์ดํด๊ฐ ๋์ง ์์์ง๋ง,
ํ
์คํธ์ฝ๋๋ฅผ ์์ฑํ๋ฉด์ ํ
์คํ
์ด ์ฉ์ดํ๋ค๊ณ ๋๋์ ์ ์๋์ 2๊ฐ์ง์๋ค.
- ํ๋์ Store๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ๋ฆฌ๋์ค๋ก ๊ด๋ฆฌํ๋ components๋ค์ ํ ์คํ ํ๊ธฐ ์ฝ๋ค.
- ๋ฆฌ๋์๋ ์์ํจ์๋ก ๊ตฌ์ฑ๋๊ณ , ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์ ๊ด๋ฆฌ๋์ด์์ด์ ๋ฆฌ๋์ ํ ์คํ , ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉํ ํ ์คํ ์ด ์ฝ๋ค.
์ด๋ฒ ๊ธ์์๋ Redux์ Reducer๋ค์ ํ ์คํธํ ๋ด์ฉ์ ๊ธฐ๋กํด๋ณด๋๋ก ํ๊ฒ ๋ค.
๐ Reducer testing
๋ฆฌ๋์ ํ
์คํ
์ state
์ action
์ ํ
์คํธ ์ผ์ด์ค์ ๋ฐ๋ผ ๋๊ฒจ์ฃผ๊ณ , ๊ทธ์ ๋ง๊ฒ ์ํ๊ฐ ์ ๋ณ๊ฒฝ๋๋์ง์ ๋ํด ํ
์คํธํ๋ ๊ฒ์ด ์ฃผ๋ฅผ ์ด๋ฃฌ๋ค.
ํ ์คํธ ์ฝ๋
์๋๋ TodoList
์ ๋ฆฌ์คํธ๋ฅผ ์ถ๊ฐ, ์๋ฃ, ์ ๊ฑฐํ๋ ๋ฆฌ๋์์ ํ
์คํธ์ฝ๋๋ค.
// todos.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { nanoid } from 'nanoid';
import { Todo } from '../types/Todo';
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Todo[],
reducers: {
add: {
prepare: (text: string) => ({
payload: {
id: nanoid(),
done: false,
text,
},
}),
reducer(state, action: PayloadAction<Todo>) {
state.push(action.payload);
},
},
toggle(state, action: PayloadAction<string>) {
const todo = state.find((todo) => todo.id === action.payload);
if (!todo) return;
todo.done = !todo.done;
},
remove(state, action: PayloadAction<string>) {
return state.filter((todo) => todo.id !== action.payload);
},
},
});
export const todosActions = todosSlice.actions;
export default todosSlice.reducer;
// todos.test.ts
import todos, { todosActions } from '../../src/_reducer/todos';
describe('todos reducer', () => {
it('has initial state', () => {
expect(todos(undefined, { type: '@@INIT' })).toEqual([]);
});
it('handles add', () => {
const state = todos([], todosActions.add('์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ'));
expect(state[0].text).toEqual('์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ');
});
it('handles toggle', () => {
const sampleState = [
{ id: '1', done: false, text: '์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ' },
{ id: '2', done: false, text: 'ํ
์คํธ ์ฝ๋ ์์ฑํ๊ธฐ' },
];
let state = todos(sampleState, todosActions.toggle('1'));
expect(state).toEqual([
{ id: '1', done: true, text: '์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ' },
{ id: '2', done: false, text: 'ํ
์คํธ ์ฝ๋ ์์ฑํ๊ธฐ' },
]);
state = todos(state, todosActions.toggle('1'));
expect(state).toEqual([
{ id: '1', done: false, text: '์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ' },
{ id: '2', done: false, text: 'ํ
์คํธ ์ฝ๋ ์์ฑํ๊ธฐ' },
]);
});
it('handles remove', () => {
let state = [
{ id: '1', done: false, text: '์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ' },
{ id: '2', done: false, text: 'ํ
์คํธ ์ฝ๋ ์์ฑํ๊ธฐ' },
];
state = todos(state, todosActions.remove('2'));
expect(state).toEqual([{ id: '1', done: false, text: '์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ' }]);
state = todos(state, todosActions.remove('1'));
expect(state).toEqual([]);
});
});
๋ฆฌ๋์๋ Reudx-toolkit
์ ์ฌ์ฉํ์ฌ ํ๋์ slice๋ก ๊ตฌ์ฑํ์๊ณ
์ด๋ฅผ ํ
์คํธ์ฝ๋์ import ํ์ฌ ๋ฆฌ๋์ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ์๋ค.
Redux-mock-store
์ด๋ ๊ฒ ๋ฆฌ๋์ค๋ก ์ ์ญ์ํ๋ฅผ ๊ด๋ฆฌํ์๋, ์ ์ญ์ํ์ ์ฐ๊ด๋ components,hooks ๋ฑ์ ํ
์คํธํ๊ธฐ ์ํด์๋
์ค์ ๋ก ์์ฑํ store
๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์ผ๋, redux-mock-store ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฏธ๋ค์จ์ด๋ฅผ ํฌํจํ ๋ค์ํ ํ
์คํธ์ฝ๋๋ฅผ ๊ฐํธํ๊ฒ ์์ฑํ ์ ์๋ค.
// prepareMockReduxWrapper.tsx
import React from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import { RootState } from '../_reducer';
function prepareMockReduxWrapper(initialState?: RootState) {
const store = configureMockStore()(initialState);
const wrapper = ({ children }: { children: React.ReactNode }) => {
return <Provider store={store as any}>{children}</Provider>;
};
return [wrapper, store] as const;
}
export default prepareMockReduxWrapper;
redux-mock-store
์ configureMockStore
๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฏธ๋ค์จ์ด ์ค์ ์ ํฌํจํ ๋ฆฌ๋์์ ์คํ ์ด๋ฅผ ๊ฐํธํ๊ฒ ์ฐ๊ฒฐํ์ฌ ํ
์คํธํ ์ ์๋ค.