일하면서 공부해욧

React, TypeScript 그리고 Jest

sognociel 2023. 5. 26. 18:29

★테스트를 작성하는 이유★

우리가 작성한 코드가 생각한 시나리오에서 정상 동작함을 보장하기 위함이다.

물론! 우리가 생각하지 못한 상황에서에 대한 동작(알 수 없는 예외)에 대해서도 최소한의 예외 처리 등을 작성하여서 코드가 실행되는 애플리케이션이 의도치 않게 종료되는 것(의도된 종료와는 별개)은 막고 보장할 수 있어야 한다.

 

코드와 프로젝트는 생명주기라는 것이 존재하는데(업데이트, 실제 사용기간 등) 그 주기 내에서 발생할 수 있는 사이드 이펙트를 체크하고 그로 인해 발생할 수 있는 문제를 실제환경이 아니더라도 재현 및 선제적으로 체크할 수 있게 하기 위해서 테스트를 작성한다는 이유를 꼭!!! 생각하면서 테스트를 작성해 보자~

 

 

프로젝트 생성

$ npx create-react-app 프로젝트이름 --template typescript

프로젝트를 생성하면 자동으로 package.json에 jest-dom과 testing-library/react가 설치되어 있는 모습을 볼 수 있다.

 

src 디렉터리에는 이미 App.test.tsx라는 파일이 있는데... 이곳에서 테스트를 하면 되는 것 같다. 매번 react 프로젝트를 만들면 지웠던 파일인데... 역시 사람은 배운 만큼 보이나 보다...

맨날 지워서 미안하다...
npm test 결과

 

 

사칙연산 만들어 테스트해 보기

일단은 컴포넌트 테스트보다 심플한 사칙연산 함수 테스트 작성을 해보는 게 좋다고 하셔서 더하기..!부터 해봤다. 그래도 1시간 안으로는 할 줄 알았는데 조금... 헤매다 보니... 꽤 시간이 걸린 듯...😂

 

CalculatorBase.ts

export const addFunction = (a: number, b: number): number => {
  if (!Number(a)) throw new Error("addFunction. Not supported parameter. a must be a number");
  if (!Number(b)) throw new Error("addFunction. Not supported parameter. b must be a number");
  return a + b;
};

 

CalculatorBase.test.ts

import { addFunction } from "./CalculatorBase";

test("addFunction", () => {
  const a = 3;
  const b = 5;
  expect(addFunction(a, b)).toEqual(a + b);
});

처음에 계속 import부분에서 문제가 있다고 에러를 띄워주더니 뭐., 가.. 어떻게 된 건진 몰라도 갑자기... 또 실행이 되어서...  ....? ?... 하면서 그냥 하늘에 감사합니다 하고 있음...

 

 

 

비동기 테스트 코드

이번에는 비동기로 데이터를 받아와서, 받아온 값을 테스트해 보는? 코드를 작성해 보았다.

데이터는 jsonplaceholder에서 user 데이터를 가져옴!

 

Fetch.ts

export const fetchData = async (url: string): Promise<any> => {
  if (!url) throw new Error("API does not exist.");
  const response = await fetch(url);
  const data = await response.json();
  return data;
};

 

Fetch.test.ts

import { fetchData } from "./Fetch";

describe("jsonplaceholder user 데이터 가져오기", () => {
  test("데이터 가져오기 성공", async () => {
    const data = await fetchData("https://jsonplaceholder.typicode.com/users");
    const data1 = data[0];

    expect(data1.name).toBe("Leanne Graham");
  });
});

 

resolves와 rejects를 사용한 방법도 있다.

test("3초 후에 받아온 나이는 30", () => {
  return expect(함수).resolves.toBe(30);
});

test("3초 후에 에러가 납니다", () => {
  return expect(함수).reject.toMatch(설정한 에러메세지);
});

toBe(30)을 사용하면 에러가 나고, toMatch(에러메시지)를 하면 pass가 된다. 에러 처리를 한 대로 에러가 나왔기 때문.

 

 

 

테스트 전후 작업

beforeEach, afterEach

let num = 0

// num의 숫자를 각 테스트 시작 전에 0으로 초기화해준다.
beforeEach(() => {
  num = 0;
});

num을 쓰는 test1
num을 쓰는 test2
....

각각의 테스트 전, 후로 실행되는 함수로 변수의 값이 변동이 되지 않기 위해? 사용해 준다.

beforeAll, afterAll도 있는데 변수를 초기화하는 것과는 달리 유저 정보를 db에서 가져오고, 끊을 때 각각의 테스트마다 할 필요 없이 최초에 한 번, 마지막에 한 번씩만 하면 되기 때문에 이 경우에는 Each보다는 All을 써주는 게 좋다.

 

.only .skip

test.only로 테스트를 진행한다면 다른 test케이스는 스킵하고 only가 붙은 하나의 경우만 테스트를 진행한다.
반대로 test.skip으로 테스트를 진행한다면 해당 test 케이스만 스킵하고 전체적으로 테스트가 진행된다.

 

 

 

mock function

테스트하기 위해 흉내만 내는 함수로 jest.fn()으로 작성하면 된다.

가짜로 대체하는 이유 : 테스트하고 싶은 기능이 다른 기능들과 엮여있을 경우(의존) 정확한 테스트를 하기 힘들기 때문이다.
request body에 사용자의 id와 password를 넣어서 post요청을 보내는 경우, 실제 데이터베이스에 사용자의 id, password를 넣는 방식으로 테스트를 하는 것은 좋은 방법이 아니다. 실제 트랜잭션이 일어나기에 IO시간도 테스트에 포함되고, 데이터베이스 연결 상태에 따라 테스트가 실패할 수도 있기 때문이다.
테스트가 실패했을 경우 코드의 문제인지, 데이터베이스의 문제인지 알기 힘들기 때문에 올바른 단위테스트라고 할 수 없다.

 

 

jest 공식 문서에 나와있는 코드로 테스트를 해보았는데, mock함수에 어떤 것들이..? 담겨있는지 확인해 보는 게 좋을 것 같아서 추가로 작성해 본다.

 

forEach

export function forEach(items: number[], callback: (item: number) => number) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

숫자 배열을 보내면 배열 안의 숫자마다 콜백함수를 적용해서 값을 return 하는..? 것...이라고 생각하면 되려나...

 

forEach.test.ts

import { forEach } from "./MockFn";

const mockCallback = jest.fn((x) => 42 + x);

describe("Mock Test", () => {
  beforeEach(() => {
    forEach([0, 1], mockCallback);
  });

  // mock 함수는 두 번 호출됨
  it("forEach mock function toHaveLength", () => {
    expect(mockCallback.mock.calls).toHaveLength(2);
  });

  // forEach의 [0][0]은 0
  it("forEach mock function [0][0] toBe 0", () => {
    expect(mockCallback.mock.calls[0][0]).toBe(0);
  });

  // forEach의 [0][1]은 1
  it("forEach mock function [0][1] toBe 1", () => {
    expect(mockCallback.mock.calls[1][0]).toBe(1);
  });

  it("forEach mock function results", () => {
    console.log(mockCallback.mock);
    expect(mockCallback.mock.results[0].value).toBe(42);
  });
});

여기서 궁금한 게 공식문서에서는 마지막 expect가 42가 될 거라고 했는데, 막상 테스트를 진행해 보니 계속 에러가 나와서 도대체 mockCallback에는 어떤 것들이 있고, 콜백함수 안에 mock의 result는 무슨 값인가?! 싶어서 console.log로 확인해 보니 아래와 같았다...

 

mockCallback

// mockCallback
[Function: mockConstructor] {
  _isMockFunction: true,
  getMockImplementation: [Function (anonymous)],
  mock: [Getter/Setter],
  mockClear: [Function (anonymous)],
  mockReset: [Function (anonymous)],
  mockRestore: [Function (anonymous)],
  mockReturnValueOnce: [Function (anonymous)],
  mockResolvedValueOnce: [Function (anonymous)],
  mockRejectedValueOnce: [Function (anonymous)],
  mockReturnValue: [Function (anonymous)],
  mockResolvedValue: [Function (anonymous)],
  mockRejectedValue: [Function (anonymous)],
  mockImplementationOnce: [Function (anonymous)],
  mockImplementation: [Function (anonymous)],
  mockReturnThis: [Function (anonymous)],
  mockName: [Function (anonymous)],
  getMockName: [Function (anonymous)]
}

// mockCallback.mock
{
  calls: [ [ 0 ], [ 1 ] ],
  instances: [ undefined, undefined ],
  invocationCallOrder: [ 1, 2 ],
  results: [
    { type: 'return', value: undefined },
    { type: 'return', value: undefined }
  ],
  lastCall: [ 1 ]
}

result의 value가 왜 undefined인지..? instance는 왜 또 undefined가 나오는지...?😭

일단. mock 프로퍼티에는 호출되었던 값들이 고스란히 저장되어 있다.

그리고. mock의 프로퍼티 중. calls는 함수가 몇 번 호출되었는지, 호출될 때 전달된 인수는 무엇인지 알 수 있다.

 

 

 

MockReturnValues

mock 함수를 이용하여 테스트 중에 테스트 값을 코드에 입력할 수도 있다.

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

처음 만들어진 mock 함수는 아무것도 가지고 있지 않지만, 추가적으로 값을 넣을 수 있는 것..!

mockReturnValueOnce는 값을 한 번만 넣을 때

mockReturnValue는 입력된 값이 호출할 때마다 계속 넣어진다.

 

아니 근데 왜 .mock.result만 확인해보려고 하면 요꼬라지...?

mockResolvedValue를 사용하면 비동기 함수를 흉내낼 수 있다! 실제로 API를 사용하지 않고 jest.mock(...) 함수를 사용하여 테스트할 수 있다는 것..!

import Chart from './Chart';

const chartMock = jest.fn();

describe('Chart Test', () => {
  it('들어온 값 테스트', async () => {
    chartMock.mockResolvedValue({male: 25, female: 75});
    const data = await doughnutMock();
    console.log(data);
    expect(data.male).toBe(25);
  });
});