์๋ฌธ
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๋ฅผ ๋ ์ฝ๊ฒ ํ ์คํธํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
๋ง์ฝ redux-saga๊ฐ ์ต์์ง ์๋ค๋ฉด redux-saga์ ๊ฐ๋ฐ์ Yassine Elouafi ์ธํฐ๋ทฐ๋ฅผ ํ์ธํด๋ณด์.
redux-saga-test-plan์ ์ฌ๊ฐ(Saga) ์ ๋๋ ์ดํฐ ํจ์๋ฅผ ํ ์คํธํ ๋, ์ค์ ๊ตฌํ ๋ก์ง๊ณผ ํ ์คํธ ์ฝ๋๊ฐ ๊ฐ๋ ์ปคํ๋ง, ๊ทธ๋ฆฌ๊ณ ๋งค๋ด์ผ ํ ํ ์คํธ์ ๋ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ค๋ค. ํ ์คํธ์ ์ ์ธ์ ์ด๊ณ , ์ฒด์ด๋(chainable) API๋ฅผ ์ ๊ณตํด์ ์ค์ ๊ตฌํ์ฒด์ธ ์ฌ๊ฐ์์ ์ํ๋ ์ดํํธ๋ง์ ํ ์คํธํ ์ ์๋๋ก ๋์์ค๋ค. ์ด์ธ ์ด๋ค ์ดํํธ๋ค์ ๋ํ๋ด๋์ง, ์ดํํธ๋ค์ ์์๋ ์ด๋ป๊ฒ ๋๋์ง ์ ๊ฒฝ์ฐ์ง ์๊ณ ๊ฑฑ์ ํ์ง ์๋๋ก ํ๋ค. redux-saga์ ๋ฐํ์์ ํจ๊ป ์ฌ์ฉํ๋ฏ๋ก, ํตํฉ ํ ์คํธ๋ฅผ ํ ์๋ ์๊ณ , redux-saga-test-plan์ ๋ด์ฅ๋ ์ดํํธ ๋ชฉํน(mocking)์ ํ์ฉํด ์ ๋ ํ ์คํธ๋ ์์ฑํ ์ ์๋ค.
๊ฐ๋จํ saga ์์๋ฅผ ๋ณด๊ณ redux-saga-test-plan์ด ๊ทธ ์ฌ๊ฐ๋ค์ ์ด๋ป๊ฒ ์ฝ๊ฒ ํ ์คํธํ๋์ง ์๊ฐํ๊ฒ ๋ค.
๋ค์์ ์ ์ ๋ชฉ๋ก์ ์์ฒญํ๋ ๊ฐ๋จํ ์ฌ๊ฐ๋ค.
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
๋ฅผ ์ฒ๋ฆฌํ๋์ง ํ์ธํ๊ณ , ๋ง๋๋ค๋ฉด ๊ฐ์ง ์ ์ ๋ชฉ๋ก์ ๋ฐํํ๋๋ก ํ๋ค.
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์ ๋ฆฌ๋์(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)
expectSaga
์์ ์ค์ง ๊ด์ฌ์ด ์๋ ์ดํํธ๋ง ํ
์คํธํ ์ ์๋ค. ๋งค๋ด์ผํ๊ฒ ์ฌ๊ฐ์ ์ดํํธ๋ค์ ๋ชจ๋ ํ์ธํ ํ์๊ฐ ์๊ณ , ๊ตฌํ ๋ก์ง๊ณผ์ ์ปคํ๋ง์ ์์จ ์ ์๋ค.type
์ ์ก์
์ put
์ดํํธ๋ฅผ ๊ทธ ์ก์
์ ํจ์ด๋ก๋์ ๊ด๊ณ์์ด ํ
์คํธํ ์ ์๋ค.๋ค์๊ณผ ๊ฐ์ด ์ฌ๊ฐ๋ฅผ ๋งค๋ด์ผํ๊ฒ ๋ฐ๋ณตํ๋ฉฐ ํ ์คํธํ๋ ๊ฒ์ ์ง์ณค์๋ค.
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์ ์์กดํ๊ณ ์์ด์ ํ์ ํ ์๋ ์๋ค. 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