redux-saga-test-plan - ๊ฐ„ํŽธํ•œ Redux Saga ํ…Œ์ŠคํŠธ - Jeremy Fairbank ์ธํ„ฐ๋ทฐ


์›๋ฌธ
Juho Vepsรคlรคinen, https://survivejs.com/blog/redux-saga-test-plan-interview/

Redux Saga๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฝ๊ธฐ๋กœ ์œ ๋ช…ํ•˜๋‹ค. ํ•˜์ง€๋งŒ, ๋” ๊ฐ„๋‹จํ•ด์งˆ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ? Jeremy Fairbank๋Š” ๋” ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด redux-saga-test-plan์„ ๋””์ž์ธํ–ˆ๋‹ค.

๋ณธ์ธ ์†Œ๊ฐœ๋ฅผ ๋ถ€ํƒํ•œ๋‹ค.

Test Double์˜ ์†Œํ”„ํŠธ์›จ์–ด ์—”์ง€๋‹ˆ์–ด์ด์ž ์ปจ์„คํ„ดํŠธ๋‹ค. ์†Œํ”„ํŠธ์›จ์–ด๊ฐ€ ๊ณ ์žฅ ๋‚˜๋ฉด ๊ทธ๊ฒƒ์„ ๊ณ ์น˜๋Š” ์ผ์„ ํ•˜๊ณ , ์ „ ์„ธ๊ณ„์˜ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ๋ฐฉ์‹์„ ๊ฐœ์„ ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•œ๋‹ค.

์•ฝ 10๋…„ ๋™์•ˆ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•ด์™”๊ณ , React์™€ Redux๊ฐ€ ํ”„๋ก ํŠธ์—”๋“œ ์„ธ์ƒ์— ๋„์›€์„ ์ค€ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ฆ๊ธฐ๊ณ  ์žˆ๋‹ค. revalidate, redux-saga-router, ๊ทธ๋ฆฌ๊ณ  ์ด ์ธํ„ฐ๋ทฐ์˜ ์ฃผ์ œ์ธ redux-saga-test-plan๊ณผ ๊ฐ™์€ React, Redux ์ƒํƒœ๊ณ„์—์„œ ์ž˜ ๋™์ž‘ํ•˜๋Š” ์˜คํ”ˆ ์†Œ์Šค ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐœ๋ฐœํ–ˆ๋‹ค.

ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ Elm์„ ์ •๋ง ์ข‹์•„ํ•œ๋‹ค. ํ˜„์žฌ๋Š” The Pragmatic Programmers์—์„œ Programming Elm: Build Safe and Maintainable Front-End Applications. ๋ผ๋Š” ์ฑ…์„ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋‹ค. ์ ˆ๋ฐ˜ ์ด์ƒ ์™„์„ฑํ–ˆ์œผ๋ฉฐ 2018๋…„ ๋ด„ ์ฆˆ์Œ ๋ณด์‹ค ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

redux-saga-test-plan์„ ์ „ํ˜€ ๋“ค์–ด๋ณธ ์ ์ด ์—†๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

redux-saga-test-plan์€ redux-saga๋ฅผ ๋” ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค.

๋งŒ์•ฝ redux-saga๊ฐ€ ์ต์ˆ™์ง€ ์•Š๋‹ค๋ฉด redux-saga์˜ ๊ฐœ๋ฐœ์ž Yassine Elouafi ์ธํ„ฐ๋ทฐ๋ฅผ ํ™•์ธํ•ด๋ณด์ž.

redux-saga-test-plan์€ ์‚ฌ๊ฐ€(Saga) ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ, ์‹ค์ œ ๊ตฌํ˜„ ๋กœ์ง๊ณผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๊ฐ–๋Š” ์ปคํ”Œ๋ง, ๊ทธ๋ฆฌ๊ณ  ๋งค๋‰ด์–ผ ํ•œ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์ค€๋‹ค. ํ…Œ์ŠคํŠธ์— ์„ ์–ธ์ ์ด๊ณ , ์ฒด์ด๋‹(chainable) API๋ฅผ ์ œ๊ณตํ•ด์„œ ์‹ค์ œ ๊ตฌํ˜„์ฒด์ธ ์‚ฌ๊ฐ€์—์„œ ์›ํ•˜๋Š” ์ดํŽ™ํŠธ๋งŒ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค. ์ด์™ธ ์–ด๋–ค ์ดํŽ™ํŠธ๋“ค์„ ๋‚˜ํƒ€๋‚ด๋Š”์ง€, ์ดํŽ™ํŠธ๋“ค์˜ ์ˆœ์„œ๋Š” ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๊ณ  ๊ฑฑ์ •ํ•˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค. redux-saga์˜ ๋Ÿฐํƒ€์ž„์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜๋„ ์žˆ๊ณ , redux-saga-test-plan์— ๋‚ด์žฅ๋œ ์ดํŽ™ํŠธ ๋ชฉํ‚น(mocking)์„ ํ™œ์šฉํ•ด ์œ ๋‹› ํ…Œ์ŠคํŠธ๋„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

redux-saga-test-plan๋Š” ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€?

๊ฐ„๋‹จํ•œ saga ์˜ˆ์‹œ๋ฅผ ๋ณด๊ณ  redux-saga-test-plan์ด ๊ทธ ์‚ฌ๊ฐ€๋“ค์„ ์–ด๋–ป๊ฒŒ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์†Œ๊ฐœํ•˜๊ฒ ๋‹ค.

๊ฐ„๋‹จํ•œ API Saga

๋‹ค์Œ์€ ์œ ์ € ๋ชฉ๋ก์„ ์š”์ฒญํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์‚ฌ๊ฐ€๋‹ค.

import { call, put } from "redux-saga/effects";

function* fetchUsersSaga(api) {
  const users = yield call(api.getUsers);
  yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
}

redux-saga-test-plan์„ ์‚ฌ์šฉํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

import { expectSaga } from "redux-saga-test-plan";

it("fetches users", () => {
  const users = ["Jeremy", "Tucker"];

  const api = {
    getUsers: () => users,
  };

  return expectSaga(fetchUsersSaga, api)
    .put({ type: "FETCH_USERS_SUCCESS", payload: users })
    .run();
});

expectSaga๋Š” ์‚ฌ๊ฐ€์™€ ๊ทธ arguments๋ฅผ ๋ฐ›๋Š” ํ•จ์ˆ˜๋‹ค. ์œ„์—์„  fetchUsersSaga์™€ api๋ฅผ ๋ชฉํ‚นํ•ด์„œ ๊ฐ€์งœ ์‘๋‹ต์„ ๋ฐ›๋„๋ก ํ–ˆ๋‹ค.

expectSaga๋Š” ์ฒด์ด๋‹์ด ๊ฐ€๋Šฅํ•œ API๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์—ฌ๋Ÿฌ ์œ ์šฉํ•œ ๋ฉ”์„œ๋“œ๋“ค์ด ์žˆ๋‹ค. ์œ„์˜ put๋ฉ”์„œ๋“œ๋Š” ์‚ฌ๊ฐ€๊ฐ€ put ์ดํŽ™ํŠธ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ํ™•์ธ(assertion) ๋ฉ”์„œ๋“œ๋กœ, FETCH_USERS_SUCCESS ์•ก์…˜์ด ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•œ๋‹ค.

run ๋ฉ”์„œ๋“œ๋Š” ์‚ฌ๊ฐ€๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค. redux-saga-test-plan์€ redux-saga์˜ run-saga ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ๊ฐ€๋Š” ์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‹คํ–‰๋˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ์ฒ˜๋ฆฌ๋œ๋‹ค. expectSaga๋Š” ์‚ฌ๊ฐ€๊ฐ€ yieldํ•˜๋Š” ๋ชจ๋“  ์ดํŽ™ํŠธ๋“ค์„ ์ถ”์ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ„์˜ put ๋ฉ”์„œ๋“œ์ฒ˜๋Ÿผ ๋ชจ๋‘ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ๊ฐ€๋Š” ๋น„๋™๊ธฐ์ด๊ธฐ ๋•Œ๋ฌธ์— redux-saga-test-plan์€ run ๋ฉ”์„œ๋“œ์—์„œ ํ”„๋กœ๋ฏธ์Šค(promise)๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋‹จ, ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์–ธ์ œ ํ…Œ์ŠคํŠธ๊ฐ€ ์™„๋ฃŒ๋˜๋Š”์ง€ ์•Œ์•„์•ผ ํ•œ๋‹ค. ์œ„์˜ ์˜ˆ์‹œ์—์„œ๋Š” Jest๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋œ๋‹ค.

redux-saga-test-plan์€ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ผ์ • ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์‚ฌ๊ฐ€๋ฅผ ํƒ€์ž„์•„์›ƒ ์‹œํ‚จ๋‹ค. ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„์€ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‚ด์žฅ๋œ ๋ชฉํ‚น

์œ„์˜ api ๊ฐ์ฒด์ฒ˜๋Ÿผ ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด, expectSaga์— ๋‚ด์žฅ๋œ ๋ชฉํ‚น ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. providers๋ผ ๋ถ€๋ฅธ๋‹ค. ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ api๋ฅผ import ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค ๊ฐ€์ •ํ•ด๋ณด์ž.

import { call, put } from "redux-saga/effects";
import api from "./api";

function* fetchUsersSaga() {
  const users = yield call(api.getUsers);
  yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
}

provide ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ชฉํ‚น์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

import { expectSaga } from "redux-saga-test-plan";
import api from "./api";

it("fetches users", () => {
  const users = ["Jeremy", "Tucker"];

  return expectSaga(fetchUsersSaga)
    .provide([[call(api.getUsers), users]])
    .put({ type: "FETCH_USERS_SUCCESS", payload: users })
    .run();
});

provide ๋ฉ”์„œ๋“œ๋Š” ๋งค์ฒ˜(matcher)-๊ฐ’(value) ์Œ์„ ๋ฐฐ์—ด๋กœ ๋ฐ›๋Š”๋‹ค. ๊ฐ ๋งค์ฒ˜-๊ฐ’ ์Œ์€ ๋งค์นญํ•  ์ดํŽ™ํŠธ์™€ ์ด์— ๋ฐ˜ํ™˜ํ•  ๊ฐ€์งœ ๊ฐ’์„ ์—˜๋ฆฌ๋จผํŠธ๋กœ ๊ฐ€์ง„ ๋ฐฐ์—ด์ด๋‹ค. redux-saga-test-plan์€ ์ดํŽ™ํŠธ๋ฅผ ๊ฐ€๋กœ์ฑ„๊ณ , ๋งค์นญ์„ ํ™•์ธํ•œ ํ›„ redux-saga์— ์ดํŽ™ํŠธ ์ฒ˜๋ฆฌ๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ๊ฐ€์งœ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค. ์ด ์˜ˆ์‹œ์—์„œ๋Š” ๋ชจ๋“  call ์ดํŽ™ํŠธ์— ๋Œ€ํ•ด์„œ api.gerUsers๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๋งž๋Š”๋‹ค๋ฉด ๊ฐ€์งœ ์œ ์ € ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค.

์ดํŽ™ํŠธ ๋””์ŠคํŒจ์น˜(Dispatching)์™€ ํฌํฌ(Fork)๋œ ์‚ฌ๊ฐ€

redux-saga-test-plan์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ณต์žกํ•œ ์‚ฌ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

import { call, put, takeLatest } from "redux-saga/effects";
import api from "./api";

function* fetchUserSaga(action) {
  const id = action.payload;
  const user = yield call(api.getUser, id);

  yield put({ type: "FETCH_USER_SUCCESS", payload: user });
}

function* watchFetchUserSaga() {
  yield takeLatest("FETCH_USER_REQUEST", fetchUserSaga);
}

watchFetchUserSaga๋Š” FETCH_USER_REQUEST์˜ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด takeLatest๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. ๋งŒ์•ฝ FETCH_USER_REQUEST๋ฅผ ๋””์ŠคํŒจ์น˜ํ•˜๋ฉด redux-saga๋Š” fetchUserSaga๋ฅผ ํฌํฌํ•˜๊ณ  ์•ก์…˜์„ ๋„˜๊ธด๋‹ค. fetchUserSaga๋Š” ์•ก์…˜์— ๋‹ด๊ธด(payload) ์•„์ด๋””๋กœ ์œ ์ € ์ •๋ณด๋ฅผ ์š”์ฒญํ•œ๋‹ค. ์ด ์ฒ˜๋ฆฌ๋ฅผ redux-saga-test-plan์„ ์ด์šฉํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

import { expectSaga } from "redux-saga-test-plan";
import api from "./api";

it("fetches a user", () => {
  const id = 42;
  const user = { id, name: "Jeremy" };

  return expectSaga(watchFetchUserSaga)
    .provide([[call(api.getUser, id), user]])
    .put({ type: "FETCH_USER_SUCCESS", payload: user })
    .dispatch({ type: "FETCH_USER_REQUEST", payload: id })
    .silentRun();
});

redux-saga-test-plan๋Š” ํฌํฌ๋œ ์‚ฌ๊ฐ€์˜ ์ดํŽ™ํŠธ๋“ค๋„ ๋ชจ๋‘ ์ถ”์ ํ•œ๋‹ค. ์œ„ ์˜ˆ์‹œ์—์„œ expectSaga๋Š” ๋‹จ์ง€ watchFetchUserSaga๋งŒ์„ ๋ฐ›์•˜์ง€๋งŒ, fetchUserSaga๊ฐ€ yieldํ•˜๋Š” put ์ดํŽ™ํŠธ๋„ ํ…Œ์ŠคํŠธํ•œ๋‹ค๋Š” ์ ์„ ์•Œ์•„๋‘์ž.

dispatch ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด FETCH_USER_REQUEST ์•ก์…˜์„ watchFetchUserSaga์— dispatchํ–ˆ๋‹ค. ์•ก์…˜์—๋Š” payload์˜ id์— 42๋ฅผ ์ง€์ •ํ–ˆ๋‹ค. redux-saga๋Š” ์ด ์•ก์…˜์„ ๋ฐ›์•„์„œ fetchUserSaga๋ฅผ ํฌํฌํ•˜๊ณ  ์‹คํ–‰ํ–ˆ๋‹ค.

takeLatest๋Š” ๋ฃจํ”„(loop)๋กœ ๋™์ž‘ํžˆ๊ธฐ ๋•Œ๋ฌธ์— redux-saga-test-plan์€ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์‚ฌ๊ฐ€๋ฅผ ํƒ€์ž„์•„์›ƒ ์‹œํ‚จ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฏธ ํƒ€์ž„์•„์›ƒ์„ ์˜ˆ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด์ง€ ์•Š๋„๋ก silentRun ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

์—ญ์ฃผ: ์ผ๋ฐ˜์ ์ธ ์‚ฌ๊ฐ€ ํ…Œ์ŠคํŠธ์—๋Š” run์„ ์‚ฌ์šฉํ•ด์„œ ๊ฒฝ๊ณ ๋ฉ”์‹œ์ง€๋ฅผ ํ™•์ธํ•ด์•ผ ํ•จ์ด ์˜ณ๋‹ค. ํ•˜์ง€๋งŒ ์œ„์˜ ์˜ˆ์‹œ์—์„œ watchFetchUserSaga์˜ takeLatest๋Š” ๋ฌดํ•œ ๋ฃจํ”„์ด๊ณ , ์ข…๋ฃŒ๊ฐ€ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ํƒ€์ž„์•„์›ƒ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด๊ณ , ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ๋žตํ•˜๋„๋ก slientRun์„ ์‚ฌ์šฉํ•œ๋‹ค.

์—๋Ÿฌ ์ฒ˜๋ฆฌ

providers๋ฅผ ์‚ฌ๊ฐ€์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค. try-catch๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ƒˆ๋กœ์šด fetchUsersSaga๋ฅผ ๋ณด์ž.

function* fetchUsersSaga() {
  try {
    const users = yield call(api.getUsers);
    yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
  } catch (e) {
    yield put({ type: "FETCH_USERS_FAIL", payload: e });
  }
}

redux-saga-test-plan/providers์—์„œ throwError๋ฅผ importํ•˜์—ฌ provide๋ฉ”์„œ๋“œ์—์„œ ์—๋Ÿฌ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

import { expectSaga } from "redux-saga-test-plan";
import { throwError } from "redux-saga-test-plan/providers";

it("handles errors", () => {
  const error = new Error("Whoops");

  return expectSaga(fetchUsersSaga)
    .provide([[call(api.getUsers), throwError(error)]])
    .put({ type: "FETCH_USERS_FAIL", payload: error })
    .run();
});

Redux์˜ ์ƒํƒœ(State)

Redux์˜ ๋ฆฌ๋“€์„œ(reducer)๋ฅผ Saga์™€ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์œ ์ € ๋ชฉ๋ก์„ ๋ฐ›์•„ ์Šคํ† ์–ด์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฆฌ๋“€์„œ๋ฅผ ๊ฐ™์ด ํ…Œ์ŠคํŠธํ•ด๋ณด์ž.

const INITIAL_STATE = { users: [] };

function reducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    case "FETCH_USERS_SUCCESS":
      return { ...state, users: action.payload };
    default:
      return state;
  }
}

withReducer ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฆฌ๋“€์„œ์— ์—ฐ๊ฒฐํ•œ ํ›„ hasFinalState๋ฅผ ์‚ฌ์šฉํ•ด ์ตœ์ข… ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค์Œ ์˜ˆ์‹œ๋ฅผ ๋ณด์ž.

import { expectSaga } from "redux-saga-test-plan";

it("fetches the users into the store state", () => {
  const users = ["Jeremy", "Tucker"];

  return expectSaga(fetchUsersSaga)
    .withReducer(reducer)
    .provide([[call(api.getUsers), users]])
    .hasFinalState({ users })
    .run();
});

ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ดํŽ™ํŠธ ๋ชฉ๋ก

๋‹ค์Œ์€ ์ดํŽ™ํŠธ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋ฉ”์„œ๋“œ ๋ชฉ๋ก์ด๋‹ค.

  • take(pattern)
  • take.maybe(pattern)
  • put(action)
  • put.resolve(action)
  • call(fn, ...args)
  • call([context, fn], ...args)
  • apply(context, fn, args)
  • cps(fn, ...args)
  • cps([context, fn], ...args)
  • fork(fn, ...args)
  • fork([context, fn], ...args)
  • spawn(fn, ...args)
  • spawn([context, fn], ...args)
  • join(task)
  • select(selector, ...args)
  • actionChannel(pattern, [buffer])
  • race(effects)

์ถ”๊ฐ€ ๊ธฐ๋Šฅ๋“ค

redux-saga-test-plan์€ ๋‹ค๋ฅธ ์†”๋ฃจ์…˜๊ณผ ๋ฌด์—‡์ด ๋‹ค๋ฅธ๊ฐ€?

  • expectSaga์—์„œ ์˜ค์ง ๊ด€์‹ฌ์ด ์žˆ๋Š” ์ดํŽ™ํŠธ๋งŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋งค๋‰ด์–ผํ•˜๊ฒŒ ์‚ฌ๊ฐ€์˜ ์ดํŽ™ํŠธ๋“ค์„ ๋ชจ๋‘ ํ™•์ธํ•  ํ•„์š”๊ฐ€ ์—†๊ณ , ๊ตฌํ˜„ ๋กœ์ง๊ณผ์˜ ์ปคํ”Œ๋ง์„ ์—†์•จ ์ˆ˜ ์žˆ๋‹ค.
  • ์„ ์–ธ์ ์ด๊ณ  ์ฒด์ด๋‹์ด ๊ฐ€๋Šฅํ•œ API๋กœ ์‚ฌ๊ฐ€๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š”๋ฐ ๊ฐ„๋‹จํ•œ ์ค€๋น„๋งŒ ํ•˜๋ฉด ๋œ๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ ๋ณธ ๋‹ค๋ฅธ ์†”๋ฃจ์…˜๋“ค์€ ๋ช…๋ นํ˜•์˜ API์™€ ๋” ๋งŽ์€ ์ค€๋น„ ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , ํŠน์ • API๋งŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ์‚ฌ๊ฐ€๋ฅผ ๋ฆฌ๋“€์„œ์™€ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋ช‡ ์•ˆ ๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜๋‹ค.
  • ์—ฌ๋Ÿฌ ๋ ˆ์ด์–ด๋กœ ๊นŠ๊ฒŒ ํฌํฌ๋œ ์‚ฌ๊ฐ€๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋‚ด์žฅ๋œ ๋ชฉํ‚น์œผ๋กœ ์ •์ /๋™์  providers๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ถ€์ • ํ™•์ธ. ์‚ฌ๊ฐ€๊ฐ€ ํŠน์ • ์ดํŽ™ํŠธ๋ฅผ yieldํ•˜์ง€ ์•Š์•˜๋Š”์ง€๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ถ€๋ถ„ ํ™•์ธ. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • type์˜ ์•ก์…˜์˜ put ์ดํŽ™ํŠธ๋ฅผ ๊ทธ ์•ก์…˜์˜ ํŒจ์ด๋กœ๋“œ์™€ ๊ด€๊ณ„์—†์ด ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

redux-saga-test-plan์„ ์™œ ๊ฐœ๋ฐœํ–ˆ๋Š”์ง€?

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ๊ฐ€๋ฅผ ๋งค๋‰ด์–ผํ•˜๊ฒŒ ๋ฐ˜๋ณตํ•˜๋ฉฐ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์— ์ง€์ณค์—ˆ๋‹ค.

function* fetchUsersSaga() {
  const users = yield call(api.getUsers);
  yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
}

it("fetches users", () => {
  const users = ["Jeremy", "Tucker"];
  const iter = fetchUsersSaga();

  expect(iter.next().value).toEqual(call(api.getUsers));

  expect(iter.next(users).value).toEqual(
    put({ type: "FETCH_USERS_SUCCESS", payload: users })
  );
});

์ด๋Ÿฐ ํ…Œ์ŠคํŠธ๋Š” ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ , ์‹ค์ œ ๊ตฌํ˜„๊ณผ ์ปคํ”Œ๋ง ๋œ๋‹ค. ์‚ฌ๊ฐ€์˜ ์ „์ฒด์ ์ธ ๋™์ž‘๊ณผ ๊ด€๊ณ„์—†๋Š” ์ดํŽ™ํŠธ ์ˆœ์„œ์˜ ์ž‘์€ ๋ณ€ํ™”๋„ ํ•ญ์ƒ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํŒจ์‹œํ‚จ๋‹ค. ์—ญ์„ค์ ์œผ๋กœ, testSaga API๋„ ๋งŒ๋“ค์—ˆ๋‹ค. ๋ช‡ ๊ฐ€์ง€ boilerplate๋ฅผ ์ œ๊ฑฐํ–ˆ์ง€๋งŒ, ์—ฌ์ „ํžˆ ์‹ค์ œ ๊ตฌํ˜„๊ณผ ์ปคํ”Œ๋ง์€ ์žˆ๋‹ค.

์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ API์™€ ๋Œ€๋ถ€๋ถ„์˜ boilerplate๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ด€์‹ฌ ์žˆ๋Š” ๋™์ž‘๋งŒ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐ ์ง‘์ค‘ํ•˜๊ธธ ์›ํ–ˆ๊ณ , ๊ทธ๋ž˜์„œ expectSaga๊ฐ€ ๋งŒ๋“ค์–ด์กŒ๋‹ค.

๋‹ค์Œ ๊ณ„ํš์€?

Elm ์ฑ…์„ ์ž‘์„ฑํ•˜๋Š”๋ฐ ๋Œ€๋ถ€๋ถ„ ์‹œ๊ฐ„์„ ๋ณด๋‚ด๊ณ  ์žˆ์–ด์„œ redux-saga-test-plan ๊ฐœ๋ฐœ์€ ์กฐ๊ธˆ ์‰ฌ๊ณ  ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค์Œ ํฐ ๊ณ„ํš์€ redux-saga ๋ฒ„์ „ 1์„ ์ง€์›ํ•˜๋Š” ๊ฒƒ์œผ๋กœ, ์ดํŽ™ํŠธ ๋ฏธ๋“ค์›จ์–ด์— ๋Œ€ํ•œ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋œ๋‹ค. ์ดํŽ™ํŠธ ๋ฏธ๋“ค์›จ์–ด๋Š” ์ดํŽ™ํŠธ๋ฅผ ๊ฐ€๋กœ์ฑ„๊ณ  ๊ฐ€์งœ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. expectSaga์˜ providers ๊ตฌํ˜„์„ ์ดํŽ™ํŠธ ๋ฏธ๋“ค์›จ์–ด๋กœ ๋” ๋‹จ์ˆœํ™”ํ•˜๊ณ ์ž ํ•œ๋‹ค.

์—ญ์ฃผ: ๋ฒˆ์—ญํ•˜๋Š” ํ˜„์žฌ redux-saga๋Š” ์•„์ง v1.0.0-beta.1 ๋ฆด๋ฆฌ์Šค ์ƒํƒœ๋‹ค.

Redux ์Šคํ† ์–ด์— ๋Œ€ํ•œ ์ „์ฒด์ ์ธ ํ†ตํ•ฉ, ์ƒˆ๋กœ์šด ๋‹จ์–ธ๋ฌธ(assertions) ๋“ฑ ๋˜ํ•œ ๊ณ„ํš์— ์žˆ๋‹ค.

์ปจํŠธ๋ฆฌ๋ทฐํ„ฐ๋ฅผ ํ™˜์˜ํ•œ๋‹ค!

redux-saga-test-plan์ด๋‚˜ ์›น ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜๋Š” ์ „๋ฐ˜์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋ณด๋Š”์ง€? ์•ž์œผ๋กœ์˜ ํŠธ๋ Œ๋“œ๋Š”?

redux-saga์— ์˜์กดํ•˜๊ณ  ์žˆ์–ด์„œ ํ™•์‹ ํ•  ์ˆ˜๋Š” ์—†๋‹ค. Mateusz Burzyล„ski์™€ ๋ชจ๋“  ์ปจํŠธ๋ฆฌ๋ทฐํ„ฐ๋“ค์€ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›Œ๋ฅญํžˆ ํ•˜๊ณ  ์žˆ๋‹ค. redux-saga๊ฐ€ v1์œผ๋กœ ํ–ฅํ•˜๋Š”๋ฐ ์ข‹์€ ์‹ ํ˜ธ๋‹ค. ํ•˜์ง€๋งŒ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์€ ๋น ๋ฅด๊ฒŒ ๋ณ€ํ•˜๊ณ  ์›€์ง์ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, RxJS๋‚˜ redux-observable์˜ ์ธ๊ธฐ๊ฐ€ ํฌ๊ฒŒ ์ƒ์Šนํ–ˆ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ redux-saga์— ๋Œ€ํ•œ ๋งŽ์€ ์ง€์›์ด ์žˆ๋Š” ํ•œ, redux-saga-test-plan์€ ํ…Œ์ŠคํŠธ ๋ถ€๋ถ„์—์„œ ๋งŽ์€ ๋„์›€์„ ์ฃผ๋„๋ก ์œ ์ง€๋  ๊ฒƒ์œผ๋กœ ์ƒ๊ฐํ•œ๋‹ค. ์‚ฌ๊ฐ€ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์€ ์–ด๋ ต๊ณ , redux-saga-test-plan์€ ๊ณ„์†ํ•ด์„œ ์ด๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด ์ฃผ๊ธธ ํฌ๋งํ•œ๋‹ค. ์ฆ‰, ๋‚˜๋Š” ๊ณ ๊ฐ์˜ ํ”„๋กœ์ ํŠธ์— ํ•ญ์ƒ redux-saga๋ฅผ ์‚ฌ์šฉํ•˜์ง„ ์•Š์ง€๋งŒ, ๋‹ค๋ฅธ ์ปจํŠธ๋ฆฌ๋ทฐํ„ฐ๋“ค์˜ ์ง€์›์œผ๋กœ redux-saga-test-plan์„ ํ…Œ์ŠคํŒ…์— ์ตœ์„ ์˜ ๋ฐฉ๋ฒ•์ด ๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํŠธ๋ Œ๋“œ์— ๋Œ€ํ•ด์„œ, ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์€ ์ •์  ํƒ€์ž…์œผ๋กœ ์œ ์ง€ ๋ณด์ˆ˜์™€ ์•ˆ์ „์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๊ฐ€๊ณ  ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. Elm, TypeScript, ๊ทธ๋ฆฌ๊ณ  Flow๋Š” ๋‹จ๋‹จํ•œ ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. ์ •์  ํƒ€์ž…์€ ๋งŽ์€ ๋‹จ์ˆœํ•œ ๋ฒ„๊ทธ์™€ ์‹ค์ˆ˜๋ฅผ ์žก์„ ์ˆ˜ ์žˆ๊ณ , ์ฝ”๋“œ๋ฅผ ๋” ์ž์‹  ์žˆ๊ฒŒ ๋ฆฌํŒฉํ† ๋งํ•˜๋„๋ก ๋„์™€์ค€๋‹ค.

์›น ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋จธ๋“ค์—๊ฒŒ ์กฐ์–ธํ•œ๋‹ค๋ฉด?

์ƒˆ๋กœ ๋‚˜์˜ค๋Š” ๋ชจ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋”ฐ๋ผ๊ฐˆ ํ•„์š”๋Š” ์—†๋‹ค. ๋‹น์‹ ์ด ๋งŒ๋“œ๋Š”, ์ข‹์•„ํ•˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์— ์ง‘์ค‘ํ•˜๋ผ. ์ตœ์‹  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•ด์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๋กœ๋ถ€ํ„ฐ ์ž์‹ ์ด ์ง„์งœ ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹ˆ๋ผ ์—ฌ๊ฒจ์ง„๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ง€ ๋ง๋ผ. ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ ๋‹น์‹ ์ด ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ ์–ธ์–ด๋ฅผ ์ดํ•ดํ•˜๊ณ , ์˜ฌ๋ฐ”๋ฅธ ์†Œํ”„ํŠธ์›จ์–ด ์—”์ง€๋‹ˆ์–ด๋ง ๋ฐฉ๋ฒ•์„ ์ง€ํ‚ค๋Š” ๊ฒƒ์ด๋‹ค. ๋‹น์‹ ์„ ๊ณต๊ฐํ•˜๊ณ  ๋•๊ณ  ์‹ถ์–ด ํ•˜๋Š” ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ผ.

๋˜ํ•œ ์ปจํผ๋Ÿฐ์Šค, ๋ฐ‹์—…์— ์ฐธ์—ฌํ•˜๋ผ. ๋•Œ๋•Œ๋กœ ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ์‚ฌ๋žŒ์ด ๊ทธ๋“ค์ด ๊ณต์œ ํ•˜๋Š” ์ฃผ์ œ์— ๋Œ€ํ•ด ์ „๋ฌธ๊ฐ€๊ฐ€ ์•„๋‹Œ์ง€(๋ณธ์ธ๋„ ๋ฌผ๋ก ์ด๋‹ค)์—๋„ ๋†€๋ž„ ๊ฒƒ์ด๋‹ค. ๋‹น์‹ ์ด ๊ธฐ์ˆ ์„ ๊ฒฝํ—˜ํ•˜๊ณ  ํ•™์Šตํ•œ ๊ฒƒ์— ํž˜๋“ค์—ˆ๋˜ ์ ์„ ๊ณต์œ ํ•  ์ˆ˜๋„ ์žˆ๊ณ , ๊ทธ ๊ธฐ์ˆ ์„ ์™œ ์ข‹์•„ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์ž์‹ ์˜ ๊ณ ์œ ํ•œ ๊ด€์ ์„ ์ œ๊ณตํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ƒˆ๋กœ์šด ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์˜๊ฐ์„ ์ฃผ๊ณ  ํž˜์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ ์ธํ„ฐ๋ทฐ๋Š” ๋ˆ„๊ฐ€ ์ข‹์„๊นŒ์š”?

๋ณธ์ธ์€ Test Double์—์„œ ์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•ฝ๊ฐ„ ํŽธ๊ฒฌ์ด ์žˆ์„ ์ˆ˜ ์žˆ์ง€๋งŒ, Justin Searls๋ฅผ ์ธํ„ฐ๋ทฐํ•˜๋ฉด ์ข‹๊ฒ ๋‹ค. ํ…Œ์ŠคํŒ…์— ๋Œ€ํ•œ ๋งŽ์€ ๋ฐœํ‘œ๋ฅผ ํ–ˆ๊ณ , ๊ทธ์˜ ํ†ต์ฐฐ๋ ฅ์ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์„ธ๊ณ„์— ํฐ ๋„์›€์ด ๋  ๊ฒƒ์ด๋‹ค. ๊ทธ๋Š” ์šฐ๋ฆฌ์˜ Test Doulbe์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ testdouble.js๋ฅผ ๊ฐœ๋ฐœํ•˜๊ณ  ์žˆ๋‹ค. testdouble.js๋Š” ํ…Œ์ŠคํŠธ์—์„œ์˜ ๋ชฉํ‚น์— ๋Œ€ํ•œ ๋‚ด ์ƒ๊ฐ์„ ๋ณ€ํ™”์‹œ์ผฐ๋‹ค.

๊ฒฐ๋ก 

์ธํ„ฐ๋ทฐ์— ์‘ํ•ด์ฃผ์–ด ๊ณ ๋ง™๋‹ค. Jeremy! redux-saga-test-plan์€ redux-saga๋ฅผ ์ž˜ ๋ณด์™„ํ•ด์ฃผ๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

๋” ๋งŽ์€ ๋‚ด์šฉ๋“ค์€ redux-saga-test-plan ์‚ฌ์ดํŠธ์™€ redux-saga-test-plan Github ํŽ˜์ด์ง€์—์„œ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋‹ค.

2017๋…„ 12์›” 20์ผ Juho Vepsรคlรคinen

์ด๋ฏผ๊ทœ2018.05.14
Back to list