[CRA+ Typescript] Jest로 비동기 로직 테스트하기

Sheryl Yun·2023년 5월 15일
0

유튜브 '코딩앙마' 님의 영상을 보고 후다닥 Jest를 공부한 다음 바로 프로젝트에 적용하여 비동기 API 호출 로직을 테스트했다!

CRA에 Jest + 타입스크립트 셋팅하기

jest와 관련된 패키지를 추가로 2개 설치한다.

  • jest-environment-jsdom
  • ts-jest (TS로 Jest를 사용하기 위해 필요)
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "axios": "^0.26.1",
    "jest-environment-jsdom": "^29.5.0", // 설치 !!
    "react": "^18.0.0",
    "react-dom": "^17.0.1",
    "react-icons": "^4.1.0",
    "react-scripts": "^5.0.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "test": "jest"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/jest": "^29.5.1",
    "@types/node": "^20.1.4",
    "@types/react": "^18.2.6",
    "@types/react-dom": "^18.2.4",
    "ts-jest": "^29.1.0", // 설치 !! 
    "typescript": "^5.0.4"
  }
}

다음에 root 경로(= package.json 경로)에 jest.config.js 파일을 만들고 다음 내용을 추가했다.

module.exports = {
  testPathIgnorePatterns: ['<rootDir>/client/node_modules/'], // 무시할 경로
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'ts-jest', // jest를 ts로 사용 가능하게
  },
  moduleNameMapper: {
    '^src/(.*)$': '<rootDir>/src/$1', // jest에서의 절대 경로 (tsconfig 절대경로 설정 안 먹음. 참고로 tsconfig의 baseUrl은 'src'였음)
  },
  testEnvironment: 'jest-environment-jsdom', // 기본 값은 node이고 원래 jsdom 쓰는데 대신 이걸 설치하고 사용하더라.. 이유는 모르겠다
};

Jest 써보기

유튜브 영상을 따라 하며 Jest를 처음부터 익혔는데 1시간 정도 따라 치다보니 감이 익혀졌다.

다음처럼 toBe와 toEqual의 차이, Promise와 async/await로 테스트하기 등을 배웠다.

// functions.ts (테스트 파일에서 사용할 함수들을 모아놓은 곳)

export const fn = {
  sum: (a: number, b: number) => a + b,
  makeUser: (name: string, age: number) => ({ name, age }),
  getName: (callback: (name: string) => void) => {
    const name = '미미';
    setTimeout(() => {
      callback(name);
    }, 2000);
  },
  getAge: () => {
    const age = 20;

    // Promise를 넘겨주면 done을 넘겨주지 않아도 기다렸다가 반환 (done은 아래 코드에서 설명)
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(age);
      }, 2000);
    });
  },
  getError: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('error');
      }, 2000);
    });
  },
};

위는 테스트를 위한 함수를 선언하는 코드이고
아래는 실제 테스트를 진행하는 코드이다.

// Search.test.ts

import { expect, test } from '@jest/globals';
import { fn } from './functions';

test('1은 1이다', () => {
  expect(1).toBe(1);
});

test('1 + 2는 4가 아니다', () => {
  expect(fn.sum(1, 2)).not.toBe(4);
});

// 객체나 배열은 재귀적으로 돌면서 값을 확인해야 하기 때문에 toBe가 아닌 toEqual을 써줘야 한다
test('이름과 나이를 전달받아 객체를 반환', () => {
  expect(fn.makeUser('코니', 14)).toEqual({
    name: '코니',
    age: 14,
  });
});

test('2초 뒤에 받아온 이름은 미미', (done) => {
  // done이 호출될 때까지 jest가 테스트를 끝내지 않고 기다려줌
  function callback(name: string) {
    try {
      expect(name).toBe('미미');
      done(); // done 호출
    } catch (error) {
      done(); // done 호출
    }
  }

  fn.getName(callback);
});

test('2초 뒤에 받아온 나이는 20', () => {
  // Promise를 호출할 때는 반드시 반환값을 return 해주어야 비동기로 작동한다
  // return을 안 해주면 안 기다리고 바로 반환
  return fn.getAge().then((age) => {
    expect(age).toBe(20);
  });

  // then문을 더 간단하게 작성하는 법: resolves, rejects라는 '매쳐' 사용
  return expect(fn.getAge()).resolves.toBe(20);
});

test('2초 후에 에러 발생', () => {
  return expect(fn.getError()).rejects.toMatch('error');
});

test('async 이용해서 2초 뒤에 나이 받아오기', async () => {
  // 위에 async가 붙으면 return 대신 await 추가
  const age = await fn.getAge();
  expect(age).toBe(20);
});

test('async와 매쳐 사용하기', async () => {
  await expect(fn.getAge()).resolves.toBe(20);
});

여기까지 학습하고 바로 실제 비동기 호출 테스트 도전..!

먼저 function 파일에 테스트를 위한 getSearchData 함수를 선언했다.

import axios from 'axios';
import { SearchData } from 'types/searchType';

export const fn = {
  getSearchData: async ({
    inputText,
    page,
    limit,
  }: {
    inputText: string;
    page: number;
    limit: number;
  }) => {
    return new Promise((resolve, reject) => {
      const result: Promise<SearchData> = axios.get(
        `${process.env.REACT_APP_API_URL}/search?q=${inputText}&page=${page}&limit=${limit}`,
        {
          headers: {
            Authorization: `Bearer ${process.env.REACT_APP_TOKEN}`,
          },
        }
      );

      resolve(result);
    });
  },
};

여기서 process.env가 안 먹힐 줄 알았는데 생각해보니 그냥 src 폴더 안이어서 잘 인식되는 내용이었다. (함수 모음 파일과 테스트 파일을 src/test 경로에 작성함)
그래서 토큰도 자연스럽게 env로 받아왔다.

이후 테스트 파일(Search.test.ts)에서 첫 페이지 데이터 받아오기 테스트 진행!

import { expect, test } from '@jest/globals';
import { fn } from './functions';

test('1번째 페이지 데이터 10개 받아오기', async () => {
  const response = await expect(
    fn.getSearchData({ inputText: 'lorem', page: 1, limit: 10 })
  );

  response.resolves.toBe([
    'Maecenas in lorem sit amet felis volutpat dapibus vulputate at dui.',
    'Nam porta lorem ut turpis pellentesque, et efficitur felis ullamcorper.',
    'Duis fringilla turpis vel lorem eleifend, sit amet hendrerit velit gravida.',
    'Cras in felis eget augue cursus placerat ac eget lorem.',
    'Sed id orci quis mi porttitor pulvinar cursus eget lorem.',
    'Fusce tincidunt lorem ac purus elementum, ut fermentum lacus mollis.',
    'Nam commodo lorem ac posuere dignissim.',
    'Etiam eu elit finibus enim consequat scelerisque aliquam vulputate lorem.',
    'Donec in lorem id eros ornare aliquam ut a nisi.',
    'Donec efficitur nulla eget lorem sollicitudin, in blandit massa dictum.',
  ]);
});

결과 값에 q, total 이런 속성도 있었는데 실제로 사용할 string 배열만 받고 싶어서 고민했다.

함수를 만들 때 반환 값과 타입 맞추는 부분이 조금 헷갈렸다.
호출은 200번이 뜨는데 toBe가 안 맞기도 하고.. axios에서 타입이 틀리기도 하고..

짜고 나서 보니까 그리 복잡한 코드가 아닌데 테스트 코드가 처음이어서 익숙치 않았던 듯 하다.

암튼 jest 배운 첫 날에 비동기 테스트 성공!

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글