[우아한테크코스 #6] 네트워크 통신 테스트

네트워크 통신을 테스트해보자

요번 유튜브 미션은 다음과 같은 명세를 따릅니다.

유튜브 검색 API를 사용하여 응답 데이터를 나의 어플리케이션에 띄워야 하기에 네트워크 통신을 하기위한 코드를 구현해야합니다 ! 다음 코드는 네트워크 요청을 수행하는 모듈입니다.

fetcher 모듈 코드

// fetcher.js
import { createUrl } from '../utils/util';

const generateFetcher =
  (API_URL) =>
  async ({ path, params }) => {
    const url = createUrl(API_URL, path, params);
    
    /* 실제 응답을 받아냅니다 */
    const response = await fetch(url, { method: 'GET' });
    
    /** 빠른 실패 */
    if (!response.ok) {
      throw new Error(`api 요청 중 에러 발생: ${response.status}`);
    }
    const data = await response.json();
    return data;
  };

export const youtubeAPIFetcher = generateFetcher('https://jolly-agnesi-fe3944.netlify.app');

generateFetcher : fetcher 모듈을 만들어내는 커링 함수입니다. API URL을 인자로 받아 해당 API URL의 END_POINT들에만 요청을 보낼 수 있는 fetcher 모듈을 만들어 반환합니다.

// 사용처

  async requestVideoById(id) {
    const videoResult = await youtubeAPIFetcher({
      // path는 엔드포인트를 말합니다.
      path: API_PATHS.GET_VIDEO,
      // 요청에 넣을 쿼리 값
      params: {
        id,
        part: 'snippet',
      },
    });

    const {
      items: [videoInfos],
    } = parserVideos(videoResult);

    return Video.create({ ...videoInfos, videoId: id });
  }

이 코드들을 테스트 해봅시다

네트워크 통신 테스트하기

유튜브 API의 경우 일일 요청 횟수에 제한이 있기 때문에 직접 요청을 보내는 것이 아닌 유튜브 API가 반환하는 데이터와 동일한 구조를 가진 데이터를 반환하는 함수로 mocking 하여 테스트를 진행해볼것입니다.

Jest를 활용한 fetcher 모듈 유닛 테스트

node 환경에서 구동되는 jest의 경우 fetch 함수가 존재하지 않기 때문에 어떠한 작업들이 필요합니다. 저는 여러 방법들 중 global 전역 객체를 수정하는 방식을 택하였습니다.

내가 선택하지 않은 다른 방식

  1. node-fetch 패키지를 설치하여, 이 패키지의 함수를 mock하여 테스트를 진행한다.
  2. 내가 API 통신에 사용하는 모듈(내가 만든)을 mocking 하기
  3. 다양한 서드파티 라이브러리들
  4. fetch 자체와 상관없는 별도의 도메인 영역을 분리해서 테스트 (API 테스트는 통합 테스트로 미루고, 우선 도메인 영역을 다루는 메소드나 로직에 집중하여 테스트를 진행)

아이디어

  • 직접 요청을 보내지 않습니다.

  • global 전역 객체에 fetch 라는 메소드를 추가하여, 클라이언트 단의 모듈에서 fetch를 호출하면 이 함수가 호출되도록 구현하였습니다.

  • fetch와 같은 조건을 구현하기 위해 Promise를 활용하여 비동기를 구현하였습니다.

테스트 코드

describe('fetcher 모듈 테스트', () => {
  
  global.API_URL = 'https://jolly-agnesi-fe3944.netlify.app';

  test('비디오 데이터를 응답한다', async () => {
    // mocking - client 단에서 사용되는 fetch 함수가 아닙니다. 이 함수는 mocking 된 함수이며, dummyVideo data를 포함한 가짜 응답 리소스를 반환하는 함수입니다.
    global.fetch = jest.fn(() =>
                         
      Promise.resolve({ ok: true, json: () => Promise.resolve(dummyVideo) })
    );
    
    // youtubeAPIFetcher 내에서 fetch 함수를 호출하여도, node 전역 객체(global)에 있는 fetch 함수가 호출되게 된다.
    const video = await youtubeAPIFetcher({
      path: API_PATHS.SEARCH,
      parmas: {
        q: 'keyword',
        part: 'snippet',
        maxResults: 10,
        type: 'video',
        pageToken: '',
      },
    });
    expect(video).toEqual(dummyVideo);
  });

  test('잘못된 요청의 경우 에러를 throw한다', () => {
    // mocking - client 단에서 사용되는 fetch 함수가 아닙니다. 이 함수는 mocking 된 함수이며, dummyVideo data를 포함한 가짜 응답 리소스를 반환하는 함수입니다.
    global.fetch = jest.fn(() =>
      Promise.resolve({ ok: false, json: () => Promise.resolve(dummyVideo) })
    );
    
    expect(
      // youtubeAPIFetcher 내에서 fetch 함수를 호출하여도, node 전역 객체(global)에 있는 fetch 함수가 호출되게 된다.
      youtubeAPIFetcher({
        path: API_PATHS.SEARCH,
        parmas: {
          q: 'keyword',
          part: 'snippet',
          maxResults: 10,
          type: 'video',
          pageToken: '',
        },
      })
    ).rejects.toThrow();
  });
});

내가 만든 모듈을 테스트한다는 의미를 주고싶다면 위와 같이 하면된다. 하지만 Unit Test에서 API fetching을 테스트 해야하는지는 의문. 도메인 영역을 다루는 다른 로직을 테스트하는 것에 더 집중해야하지 않을까? (API Fetching에 대한 테스트는 E2E Test로 미루고)

Reference

Cypress를 활용하여 E2E Test

Jest에서는 fetcher 모듈에 대한 유닛테스트를 진행했다면, E2E Test에서는 API 응답 데이터를 잘 가져와 사용자가 보는 화면에 잘 띄워주는지를 테스트 해야합니다.

아이디어
jest와 유사하게 Test에서 내 API Key에 할당된 요청 횟수를 사용하기 싫었습니다. 그렇기 때문에 다음과 같은 아이디어를 토대로 개발을 진행하였습니다.

  • 진짜 요청은 보내지 않습니다.

  • API Request를 가로채 mock Data를 반환하도록 합니다.

테스트 코드
1. fixture 데이터들 (실제 요청으로 얻어내는 응답과 같은 구조를 가진 mock 데이터)

  1. custom command + e2e test code
// cypress/support/command.js
Cypress.Commands.add('interceptAPIRequest', (PATH) => {
  const API_URL = 'https://jolly-agnesi-fe3944.netlify.app';
  if (PATH === API_PATHS.SEARCH) {
    // 첫번째 인자로 가는 요청을 인터셉트하여 두번째 인자에 설정되어 있는 `fixture` 데이터를 반환한다.
    return cy.intercept(`${API_URL}/${PATH}*`, { fixture: 'searchResult' }).as(PATH);
  }
  if (PATH === API_PATHS.GET_VIDEO) {
    return cy.intercept(`${API_URL}/${PATH}*`, { fixture: 'video' }).as(PATH);
  }
});

// cypress/integration/search.test.js
describe('사용자는 검색을 통해 영상을 확인할 수 있다.', () => {
  const baseURL = 'http://localhost:9000/';
  beforeEach(() => {
    cy.visit(baseURL);
    cy.showModal();
  });

  it('모달 버튼을 클릭 후, 검색어를 입력하면 결과를 확인할 수 있다.', () => {
    const validKeyword = '정상검색';

    // API_URL + API_PATH(END_POINT)으로 가는 요청을 intercept하는 커스텀 커맨드 입니다.
    cy.interceptAPIRequest(API_PATHS.SEARCH);

    cy.get('#search-input-keyword').type(validKeyword);
    cy.get('#search-button').click();

    cy.get('.video-item').should('exist');
  });
});

Reference

좋은 웹페이지 즐겨찾기