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');
  });
});

다 끝났습니다! 축하합니다. 이제 여러분들도, 튼튼한 코드를 작성 할 준비가 끝났습니다 :)

results matching ""

    No results matching ""