[エレガントな技術ルート#6]ネットワーク通信のテスト


ネットワーク通信をテストします


今回のYouTubeタスクは以下の通りです.

YouTube検索APIを使用して応答データを私のアプリケーションに送信する必要があります.そのため、ネットワーク通信のためのコードを実装する必要があります.次のコードは、ネットワーク要求を実行するモジュールです.

グリップモジュールコード

// 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モジュールのフック関数.作成してfetcherモジュールに戻り、API URLをパラメータとして受け入れ、API URLのEND POINTのみに要求を送信する.
// 사용처

  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 });
  }
これらのコードをテストしてみましょう.

ネットワーク通信のテスト


YouTube APIでは、毎日のリクエスト数が限られているため、直接リクエストを送信するのではなく、mockingをデータを返す関数として使用してみます.

Jestテストを使用してモジュールユニットをキャプチャする

node環境で動作するjestの場合、fetch関数は存在しないため、いくつかの動作が必要である.globalグローバルオブジェクトを変更する方法を多くの方法で選択しました.
他に選択肢はない
  • node-fetchパッケージをインストールし、そのパッケージの関数をシミュレートしてテストします.
  • API通信で使用したモジュール(作成した)
  • をシミュレート
  • 各種サードパーティライブラリ
  • テスト
  • 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

  • jest - async testing

  • jest - using node-fetch in test
  • CypressによるE 2 Eテスト

    Jestfetcherモジュールのユニットテストを行った場合、E 2 EテストはAPI応答データが画面によく表示されるかどうかをテストする必要があります.
    アイデアjestと同様に、割り当てられたAPI鍵を使用する要求数をテストしたくない.そこで、以下のアイデアに基づいて開発を行いました.

  • 私は本当の要求を出さない.
  • API Requestを横棒としてmock Dataを返します.
  • テストコード
    1.fixtureデータ(実際のリクエストの応答と同じ)
  • mock + custom command
  • // 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
  • cypress network-test