5. 비동기 작업 테스트
대부분의 웹어플리케이션은 Ajax 요청을 합니다. 이러한 비동기 작업 또한 테스트를 해줄 수 있습니다. 기본적인 방법으로는, 테스트 과정에서 로직에서 사용하는 실제 주소에 HTTP 요청을 날렸다가, 기다린다음에 잘 됐는지 확인하는 방법이 있는데, 딱히 효율적이진 않습니다. - 네트워크를 통해서 데이터를 가져오게 된다면, 상황에 따라 서버의 값이 바뀔 수도 있고, 딜레이도 있습니다. 그러면, API 를 요청하는 테스트가 늘어날수록, 테스트도 오래 걸리게 되겠죠.
그 대신에, 우리는 nock 이라는 (혹은 비슷한 류의) 라이브러리를 사용합니다. 이 라이브러리는, 우리가 사전에 정해준 주소로 요청을 하게 되었을 때, HTTP 요청을 가로채서, 네트워크 요청을 실제로 넣지 않고 우리가 원하는 데이터가 바로 나오도록 설정해줄수있습니다.
우선 이 라이브러리를 설치해주세요.
$ yarn add nock
그리고, axios 의 어댑터를 http 로 설정해주어야 합니다. setupTests.js 를 다음과 같이 수정하세요. (그래야 nock이 가로챌수있습니다)
src/setupTests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import http from 'axios/lib/adapters/http';
import axios from 'axios';
axios.defaults.adapter = http;
configure({ adapter: new Adapter() });
액션 테스트
우선, thunk 가 제대로 작동하는지 (요청이 시작했을때, 실패했을 때, 성공했을때 모든게 잘 되는지) 확인을 해보겠습니다.
src/store/modules/post.test.js
import post, { getPost } from './post';
import nock from 'nock';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
describe('post', () => {
describe('actions', () => {
const store = configureMockStore([thunk])();
it('getPost dispatches proper actions', async () => {
nock('http://jsonplaceholder.typicode.com')
.get('/posts/1').once().reply(200, {
title: 'hello',
body: 'world'
});
await store.dispatch(getPost(1));
expect(store.getActions()[0]).toHaveProperty('type', 'post/GET_POST_PENDING');
expect(store.getActions()[1]).toHaveProperty('type', 'post/GET_POST_SUCCESS');
expect(store.getActions()).toMatchSnapshot();
});
it('fails', async () => {
store.clearActions(); // 기존 액션 비우기
nock('http://jsonplaceholder.typicode.com')
.get('/posts/0').once().reply(400);
try {
await store.dispatch(getPost(0));
} catch (e) {
}
expect(store.getActions()).toMatchSnapshot();
});
});
});
리듀서 테스트
리듀서가 제대로 작동하는지 테스트 하기 위하여, 리듀서 함수를 직접 호출하는 대신, 스토어를 만들어서 실제로 변화가 제대로 이뤄지는지 검증해보겠습니다.
src/modules/post.test.js
import post, { getPost } from './post';
import nock from 'nock';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import configureStore from '../configureStore';
describe('post', () => {
describe('actions', () => {
const store = configureMockStore([thunk])();
it('getPost dispatches proper actions', async () => {
nock('http://jsonplaceholder.typicode.com')
.get('/posts/1').once().reply(200, {
title: 'hello',
body: 'world'
});
await store.dispatch(getPost(1));
expect(store.getActions()[0]).toHaveProperty('type', 'post/GET_POST_PENDING');
expect(store.getActions()[1]).toHaveProperty('type', 'post/GET_POST_SUCCESS');
expect(store.getActions()).toMatchSnapshot();
});
it('fails', async () => {
store.clearActions(); // 기존 액션 비우기
nock('http://jsonplaceholder.typicode.com')
.get('/posts/0').once().reply(400);
try {
await store.dispatch(getPost(0));
} catch (e) {
}
expect(store.getActions()).toMatchSnapshot();
});
});
describe('reducer', () => {
const store = configureStore();
it('should process getPost', async () => {
nock('http://jsonplaceholder.typicode.com')
.get('/posts/1').once().reply(200, {
title: 'hello',
body: 'world'
});
await store.dispatch(getPost(1));
expect(store.getState().post.title).toBe('hello');
});
});
});
컨테이너 컴포넌트 테스트
자! 마지막 테스트 코드를 작성해보겠습니다. 이번 테스트 코드에서도 실제 스토어를 사용하구요, 컴포넌트를 렌더링하고, 내부에 있는 버튼을 클릭하겠습니다.
src/containers/PostContainer.js
import React from 'react';
import { mount } from 'enzyme';
import PostContainer from './PostContainer';
import configureStore from '../store/configureStore';
import nock from 'nock';
import { Provider } from 'react-redux';
describe('PostContainer', () => {
let component = null;
const store = configureStore();
const context = { store };
it('renders correctly', () => {
component = mount(
<Provider store={store}>
<PostContainer />
</Provider>
);
});
it('fetches and updates', async () => {
nock('http://jsonplaceholder.typicode.com')
.get('/posts/1').once().reply(200, {
title: 'hello',
body: 'world'
});
component.find('button').simulate('click');
});
});
우리가 이전에 리듀서를 테스트 할 땐, getPost 를 직접 호출했었기 때문에 await 을 할 수있었는데요, 지금의 경우, 버튼에 클릭 이벤트를 시뮬레이트를 했을 때, 딱히 getPost 가 반환하는 Promise 에 접근 할 방법이 없습니다.
그 대신에, 스토어의 subscribe 기능을 활용해서, 새로운 Promise 를 만들고, subscribe 를 통하여 새 액션이 디스패치 됐을 때 resolve 를 하도록 합니다. (subscribe 함수의 파라미터에 우리가 만든 함수를 정해주면, 새 액션이 디스패치 될 때마다 파라미터로 넣어준 함수가 호출됩니다.)
src/containers/PostContainer.js
import React from 'react';
import { mount } from 'enzyme';
import PostContainer from './PostContainer';
import configureStore from '../store/configureStore';
import nock from 'nock';
import { Provider } from 'react-redux';
describe('PostContainer', () => {
let component = null;
const store = configureStore();
const context = { store };
it('renders correctly', () => {
component = mount(
<Provider store={store}>
<PostContainer />
</Provider>
);
});
it('fetches and updates', async () => {
nock('http://jsonplaceholder.typicode.com')
.get('/posts/1').once().reply(200, {
title: 'hello',
body: 'world'
});
component.find('button').simulate('click');
const waitForNextAction = new Promise(resolve => {
const unsubscribe = store.subscribe(() => {
resolve();
unsubscribe();
});
});
await waitForNextAction;
expect(component.find('h2').text()).toBe('hello');
expect(component.find('p').text()).toBe('world');
});
});
다 끝났습니다! 축하합니다. 이제 여러분들도, 튼튼한 코드를 작성 할 준비가 끝났습니다 :)