๊ฐœ์ธ๊ณต๋ถ€/TDD

React Testing: Reducer testing (with Redux)

soon327 2021. 11. 17. 23:21


๐Ÿง Redux๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๋‹ค?

๋ฆฌ์•กํŠธ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ,
์ „์—ญ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ฆฌ๋•์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์ค‘ ํ•˜๋‚˜๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๋‹ค๋ผ๋Š” ๊ฒƒ์ด๋‹ค.
ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด๊ธฐ ์ „์—๋Š” ์–ด๋–ค ์ ์—์„œ ์‰ฝ๋‹ค๋Š” ๊ฑด์ง€ ์ž˜ ์ดํ•ด๊ฐ€ ๋˜์ง€ ์•Š์•˜์ง€๋งŒ,
ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ํ…Œ์ŠคํŒ…์ด ์šฉ์ดํ•˜๋‹ค๊ณ  ๋Š๋‚€์ ์€ ์•„๋ž˜์˜ 2๊ฐ€์ง€์˜€๋‹ค.

  1. ํ•˜๋‚˜์˜ Store๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฌ๋•์Šค๋กœ ๊ด€๋ฆฌํ•˜๋Š” components๋“ค์„ ํ…Œ์ŠคํŒ…ํ•˜๊ธฐ ์‰ฝ๋‹ค.
  2. ๋ฆฌ๋“€์„œ๋Š” ์ˆœ์ˆ˜ํ•จ์ˆ˜๋กœ ๊ตฌ์„ฑ๋˜๊ณ , ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์ž˜ ๊ด€๋ฆฌ๋˜์–ด์žˆ์–ด์„œ ๋ฆฌ๋“€์„œ ํ…Œ์ŠคํŒ…, ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•œ ํ…Œ์ŠคํŒ…์ด ์‰ฝ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” 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๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฏธ๋“ค์›จ์–ด ์„ค์ •์„ ํฌํ•จํ•œ ๋ฆฌ๋“€์„œ์™€ ์Šคํ† ์–ด๋ฅผ ๊ฐ„ํŽธํ•˜๊ฒŒ ์—ฐ๊ฒฐํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ™ ์ฐธ๊ณ