현재 진행중인 프로젝트에 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);
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' }),
);
}
}
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
를 결과값으로 반환합니다.
put({ type: "EMAIL_FIND/loadSuccess" })
put
메서드는 EMAIL_FIND/loadSuccess
액션이 발생했는지 확인해줍니다.
withReducer(findEmailReducer)
redux state
를 redux-saga
와 함께 테스트 하기위해 withReducer
메서드를 사용해서 리듀서에 연결해줍니다.
hasFinalState({ FindEmailLoading: false, FindEmailDone: true, FindEmailError: null, })
hasFinalState
를 사용해 redux store
의 최종적인 상태를 확인할 수 있습니다.
사가를 실행시키는 방법에는 run
과 silentRun
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)
를 반환해주면 됩니다.
테스트코드에 맞춰 코드를 작성해보았습니다
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],
);