redux-saga test(feat. redux-saga-test-plan)

마데슾 : My Dev Space·2020년 8월 27일
1

현재 진행중인 프로젝트에 TDD를 사용해 redux-saga를 적용시켜보려 합니다.

redux-saga 테스팅을 위해 redux-saga-test-plan 라이브러리를 선택하게되었는데요. 해당글에서 redux-saga-test-plan를 사용해 redux-saga를 테스팅하는 과정을 보여드리도록 하겠습니다.

먼저, redux-saga-test-plan를 설치해줍니다.
npm i -D redux-saga-test-plan

그리고, 테스트 코드를 아래와같이 작성해줍니다.

이메일 찾기 성공 테스트 코드

먼저 이메일 찾기에 성공했을 경우의 테스트코드를 작성하였습니다.

describe('redux saga test', () => {
  it('find email success => ', () => {
    const data = 'test@naver.com';
    return expectSaga(watchRequestFindEmail)
      .withReducer(findEmailReducer)
      .dispatch({ type: "EMAIL_FIND/load", payload: {data} }) // 1,2
      .provide([[call(findEmailApi, data), true]]) // 3
      .put({ type: "EMAIL_FIND/loadSuccess" }) // 4
      .hasFinalState({
        FindEmailLoading: false,
        FindEmailDone: true,
        FindEmailError: null,
      })
      .silentRun();
  });
});

코드를 설명하겠습니다.

expectSaga()는 첫번째인자로 테스트를 실행할 함수, 두번째인자로는 그 함수의 arguments를 받는 함수입니다. expectSaga()가 실행되면 테스트에 유용하게 사용될 수 있는 메서드들을 반환합니다.

⬇⬇⬇ watchRequestFindEmail 함수 코드 ⬇⬇⬇

yield takeLatest(findEmailAction.load, findEmail);
  1. dispatch({type: "EMAIL_FIND/load", payload: {data}})
    watchRequestFindEmail 함수 에서는 takeLatest를 사용해 가장 마지막 액션을 처리해주고있다. 그렇기때문에 EMAIL_FIND/load 액션을 dispatch하면 redux saga에서 해당액션을 기다리고 있다 잡아서 findEmail 함수를 액션과 함께 실행시킨다.

⬇⬇⬇ findEmail 함수 코드 ⬇⬇⬇

export function* findEmail(action) {
  try {
    yield call(findEmailApi, action.payload.data);
    yield put(findEmailAction.loadSuccess());
  } catch (e) {
    yield put(
      findEmailAction.loadFailure({ error: 'Email authentication failed. please try again' }),
    );
  }
}
  1. provide([[call(findEmailApi, data), true]])

    provide 메서드는 매처(matcher)-값(value) 쌍을 배열로 받는다.
    매처-값 쌍은 매칭할 이펙트와 이에 반환할 가짜 값을 엘리먼트로 가진 배열이다. redux-saga-test-plan은 이펙트를 가로채고, 매칭을 확인한 후 redux-saga에 이펙트 처리를 넘기지 않고 바로 가짜 값을 반환하도록 한다.

    위의 내용을 참고하여 제가 사용한 코드를 분석해보자면,
    provide([[call(findEmailApi, data), true]]) 코드는 call 이펙트가 실행되면 redux-saga-test-plan이 이펙트를 가로채서 findEmailApi를 사용하고 있다는것이 확인이 되면 테스트를 위해 가짜로 만들어준 true를 결과값으로 반환합니다.

  2. put({ type: "EMAIL_FIND/loadSuccess" })
    put 메서드는 EMAIL_FIND/loadSuccess 액션이 발생했는지 확인해줍니다.

  3. withReducer(findEmailReducer)
    redux stateredux-saga와 함께 테스트 하기위해 withReducer 메서드를 사용해서 리듀서에 연결해줍니다.

  4. hasFinalState({ FindEmailLoading: false, FindEmailDone: true, FindEmailError: null, })

    hasFinalState를 사용해 redux store의 최종적인 상태를 확인할 수 있습니다.

  5. 사가를 실행시키는 방법에는 runsilentRun 2가지의 메서드가 있습니다.
    일반적으로 사가 테스트에서는 run을 사용해서 경고메시지를 확인하는 것이 맞지만 takeLatest를 사용하는 경우에는 silentRun를 사용합니다.
    그 이유는..!👆
    takeLatest는 무한 루프를 돌기때문에 redux-saga-test-plan에서 경고 메시지와 함께 사가를 타임아웃 시키는데, 이때 slientRun를 사용하면 경고 메시지를 생략할 수 있습니다.

이메일 찾기 실패 테스트 코드

이번엔 이메일 찾기에 실패했을 경우의 테스트코드를 작성해보았습니다.

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

it('find email failure => ', () => {
    const data = 'test@naver.com';
    const error = new Error('Whoops');

    return expectSaga(watchRequestFindEmail)
      .withReducer(findEmailReducer)
      .dispatch(findEmailAction.load({ data }))
      .provide([[call(findEmailApi, data), throwError(error)]])
      .put({ type: "EMAIL_FIND/loadSuccess", payload:{ error: 'Email authentication failed. please try again' }})
      .hasFinalState({
        FindEmailDone: false,
        FindEmailLoading: false,
        FindEmailError: 'Email authentication failed. please try again',
      })
      .silentRun();
  });

성공했을 경우의 테스트코드와 거의 동일하기 때문에 다른부분에 대해서만 설명하도록 하겠습니다.

tyr catch문을 사용할 경우 테스트코드에서는 아래와같이 에러를 발생시켜줄 수 있습니다.

const error = new Error('Whoops'); 에러코드를 만들어준 후
provide([[call(findEmailApi, data), throwError(error)]]) 이와같이 목킹함수의 결과값으로 throwError(error)를 반환해주면 됩니다.

Saga 코드

테스트코드에 맞춰 코드를 작성해보았습니다

import { all, call, fork, put, takeLatest } from 'redux-saga/effects';
import { findEmailAction } from './FindPasswordFormSlice';
import findEmailApi from './FindPasswordFormApi';

export function* findEmail(action) {
  try {
    yield call(findEmailApi, action.payload.data);
    yield put(findEmailAction.loadSuccess());
  } catch (e) {
    yield put(
      findEmailAction.loadFailure({ error: 'Email authentication failed. please try again' }),
    );
  }
}

export function* watchRequestFindEmail() {
  yield takeLatest(findEmailAction.load, findEmail);
  // 해당부분이 액션과 함께 또다른함수(findEmail)를 실행시켜주는 부분입니다!
}

export default function* userSaga() {
  yield all([fork(watchRequestFindEmail)]);
}

테스트 코드에서 dispatch 메서드를 통해 EMAIL_FIND/request 액션을 디스패치하는 부분은 아래와같이 작성되어집니다.

const findEmailBtnHandler = useCallback(
  (e: KeyboardEvent) => {
    if (email !== '' && e.key === 'Enter') {
      dispatch({ type: "EMAIL_FIND/request", payload: {data} });
    }
  },
  [email],
);

참고글

profile
👩🏻‍💻 🚀

0개의 댓글