React에서 단위 테스트 작성하기

landvibe
6 min readOct 30, 2016

--

현재 프로젝트에서 사용 중인 테스트 관련 라이브러리는 다음과 같다.

  • mocha: 자바스크립트 테스트 프레임워크다. 테스트를 실행하고 결과를 출력한다
  • expect: 다양한 assertion 구문을 지원한다. 단순 equal(===) 구문 외에 다양한 assertion 기능이 필요할 때 사용한다.
  • nock: http mock을 지원한다. 특정 url에 결과값을 등록하면 그 url로 요청할 때마다 항상 등록된 결과값이 나오게 할 수 있다.
  • enzyme: react 테스트에 특화된 기능을 제공한다.
  • redux-mock-store: redux async action을 테스트할 때 사용된다.

각각의 라이브러리를 따로 설명하지 않고 각 테스트 상황별로 알아보자.

Element 개수 테스트

ui를 테스트하는 간단한 방법 중의 하나는 렌더링 후에 특정 element가 실제 x개 존재하는지 검사하는 것이다.

const wrapper = shallow(<App />);
expect(wrapper.find(“tr”).length).toBe(3);
  • enzyme의 shallow 렌더링을 사용했다. full 렌더링은 느리므로 꼭 필요한 경우가 아니라면 shallow 렌더링을 사용하자.
  • find에는 문자열 외에도 컴포넌트를 입력으로 넣을 수 있다.

자식 컴포넌트로 넘겨준 함수가 호출되는지 테스트

자식 컴포넌트에서 발생한 이벤트를 받고 싶을 때 props로 함수를 넘기는 경우가 많다. 이벤트가 발생했을 때 실제로 그 함수가 호출되는지 테스트한다.

const someFunction = expect.createSpy();
const wrapper = shallow(<App onEvent1={someFunction} />);
wrapper.instance().onSomeAction();
expect(someFunction.calls.length).toBe(1);
expect(someFunction.calls[0].arguments[0]).toBe(‘abc’);
  • App 컴포넌트의 멤버 함수인 onSomeAction 내부에서 onEvent1을 호출한다.
  • expect의 spy를 이용하면 함수가 어떤 파라미터와 함께 실행됐는지 알 수 있다.

컴포넌트를 특정 state로 만든 후 테스트

react에서는 state 변수로 컴포넌트 내부 상태를 관리한다. 따라서 다양한 state값에 대해 테스트할 필요가 있다.

const wrapper = shallow(<App />);
wrapper.setState({
someKey: 100,
});
wrapper.instance().increaseSomeKey();
expect(wrapper.state().someKey).toBe(101);

특정 함수의 리턴값을 고정후 테스트

테스트하려는 함수 내부에서 다른 함수를 호출할 때 그 내부 함수의 리턴값이 전체 결과에 영향을 미친다. 내부 함수의 리턴값을 고정하면 테스트가 편해진다.

const wrapper = shallow(<App />);
const myFunc = expect.createSpy().andReturn(10);
wrapper.instance().getIncreaseAmount = myFunc;
wrapper.instance().increaseSomeKey();
expect(wrapper.state().someKey).toBe(10);
  • spy의 andReturn 함수를 이용해서 리턴값을 고정한다.
  • getIncreaseAmount라는 멤버 함수를 다른 함수로 교체하는 부분을 눈여겨보자

Redux async action 테스트하기

redux action에서 서버 통신이 필요할 때 thunk 라이브러리를 사용한다. 보통 서버 통신 과정을 여러 action으로 분리하게 되므로 하나의 async action을 호출하면 여러 action이 순차적으로 발생한다. 이럴 때 redux-mock-store와 nock을 사용하면 편리하다.

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../actions'
import * as codes from '../actions/actionCodes'
import nock from 'nock'
import expect from 'expect'
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('test actions', () => {
afterEach(() => {
nock.cleanAll();
});
it('test async action 1', done => {
const queryString = '/get_data';
const data = [{name: 'a'}, {name: 'b'}];
nock('http://localhost:8000')
.get(queryString)
.reply(200, data);
const expectedActions = [
{type: codes.Set_Is_Fetching, isFetching: true},
{type: codes.Set_Page_Params_Data, data: data},
{type: codes.Set_Is_Fetching, isFetching: false},
];
const store = mockStore({
mainPage: {
}
});
store.dispatch(actions.fetchData(queryString))
.then(() => {
expect(store.getActions()).toEqual(expectedActions)
}).then(done).catch(done);
});
...
  • nock을 사용해서 http://localhost:8000/get_data를 호출하면 항상 [{name: ‘a’}, {name: ‘b’}]를 리턴하도록 설정한다
  • redux-mock-store을 사용해서 fetchData async action 호출 후 3개의 action이 발생하는지 체크한다
  • mocha의 afterEach를 통해서 각 it 블록이 끝나면 nock을 초기화한다
  • expect의 toEqual은 object의 내부 값까지 모두 비교한다

정리하며

부끄러운 얘기지만 프로그래밍을 처음 시작하고 수년 동안은 단위 테스트를 작성하지 않았다. 돌이켜보면 단위 테스트를 작성하지 않았기 때문에 그때 그토록 디버깅에 많은 시간을 썼던 것 같다. 단위 테스트를 작성하면 테스트 코드를 관리하는 비용이 크기 때문에 작성하지 않는다고 말하는 사람들이 있는데 대체로 틀린 말이다. 거의 대부분의 경우 단위 테스트 코드의 관리 비용보다 테스트 코드 덕분에 얻는 이득이 더 많다.

--

--

No responses yet