본문 바로가기

TDD

Jest 많이 썼던 내용들 정리 (1)

반응형

 

Intro

이 포스팅은 Jest 공식문서를 보고 공부한 내용들이 아니라 TDD를 하면서 많이 썼던 내용들이나 헷갈렸던 내용들을 정리할 필요가 있다고 생각해서 정리해봤다

 

 

toBe

toBe는 정리할 내용도 없지만 그래도 가장 기본적인 메서드이기 때문에 정리해봤다

toBe는 Jest의 매처 중 하나로, strict equality를 검사한다. 즉 JavaScriptdml === 연산자와 동일하게 작동하며, primitive value들을 비교할 때 주로 사용하고 있다

 

toBe 예시

describe("toBe", () => {
  it("sum", () => {
    expect(sum(1, 2)).toBe(3);
    expect(sum(1, 2)).not.toBe(4);
  });

  it("subtract", () => {
    expect(subtract(2, 1)).toBe(1);
    expect(subtract(2, 1)).not.toBe(2);
  });
});

 

 

toBe를 사용할 때 주요 포인트로는 value가 기대되는 값과 엄격히 동일한지 검사한다는 점이다.

즉 expect(sum(1, 2)).toBe('3'); 는 실패하는 코드이다

 

 

 

비동기 함수 테스트

비동기 함수를 테스트하는 방법은 여러가지가 있는데 내가 지금까지 봐오거나 사용한 코드로는 대표적으로 3가지가 있다.

  • resolve를 이용한 테스트
  • then문을 이용한 테스트
  • await을 이용한 테스트

사실 resolve를 이용한 테스트는 사용한 적은 거의 없는 거 같은데, 은근 보이길래 같이 정리해봤다

 

resolve 예시

// asyncFunction.ts
export const asyncFunction = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("resolved value");
    }, 1000);
  });
};

// asyncFunction.test.ts
import { asyncFunction } from "./asyncFunction";

test("asyncFunction resolves with correct value", () => {
  return expect(asyncFunction()).resolves.toBe("resolved value");
});

 

 

then을 이용한 테스트 예시

// asyncFunction.ts
export const asyncFunction = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("resolved value");
    }, 1000);
  });
};

// asyncFunction.test.ts
import { asyncFunction } from "./asyncFunction";

test("asyncFunction resolves with correct value", () => {
  return asyncFunction().then((data) => {
    expect(data).toBe("resolved value");
  });
});

 

 

await을 이용한 테스트 예시

// asyncFunction.ts
export const asyncFunction = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("resolved value");
    }, 1000);
  });
};

// asyncFunction.test.ts
import { asyncFunction } from "./asyncFunction";

test("asyncFunction resolves with correct value", async () => {
  const data = await asyncFunction();
  expect(data).toBe("resolved value");
});

 

 

Jest LifeCycle

Jest의 생명주기 메서드는 테스트 실행 전후특정 작업을 수행할 수 있게 해준다. 라이프 사이클을 이용해서 테스트 환경을 설정하거나 반대로 정리도 가능하다. 내가 사용할 때는 대부분 mocking된 함수를 정리할 때 대표적으로 많이 사용하는 것 같다.

 

생명주기 메서드

  • beforeAll: 모든 테스트가 실행되기 전에 한 번만 실행된다
  • beforeEach: 각 테스트(it 블록)가 실행되기 전에 실행된다
  • afterAll: 모든 테스트가 실행된 후 한 번만 실행된다
  • afterEach: 각 테스트(it 블록)가 실행된 후 실행된다

 

생명주기 메서드 외에도 의외로 많이 쓰는 메서드도 있다

todo와 skip인데

todo는 아직 구현되지 않은 테스트를 표시할 때 사용하는데 문자 그대로 앞으로 할 구현할 메서드를 todo로 표시해두면 테스트할 때 실행이 되지 않는다

skip 또한 문자 그대로 테스트를 스킵할 때 사용한다. 에러나 버그가 생겨 급하게 코드를 push를 해야 되는데 테스트 단계에서 빌드가 실패해 배포가 안되는 상황이 있을 수 있기 때문에 급하게 수정한 코드는 skip을 통해서 테스트를 패싱처리 한다

 

describe("Jest LifeCycle", () => {
  beforeAll(() => {
    console.log("beforeAll");
  });

  beforeEach(() => {
    console.log("beforeEach");
  });

  afterAll(() => {
    console.log("afterAll");
  });

  afterEach(() => {
    console.log("afterEach");
  });

  it.todo("test 1"); // 이 테스트는 실행되지 않음

  it.skip("test 2", () => { // 이 테스트는 건너뛰어짐
    console.log("test 2");
  });
});

 

 

 

jest.spyOn

spyOn은 객체의 메서드를 감시하고 해당 메서드가 호출되었는지, 호출 횟수, 호출된 인수 등을 추적할 수 있게 해준다. 또한 해당 메서드의 구현을 모킹하여 원하는 동작을 정의할 수도 있다.

 

감시한다고 하면 뭘 감시하고 어떻게 사용하는지 감이 안 올 수 있고, 실제로 내가 그랬다.. 남들도 그랬는지는 모르겠지만 '감시'라는 단어에 꽂히지 말고 spyOn을 우선 모킹하는데 사용을 하다보면 '감시'라는 단어가 왜 나왔는지 알 수 있다

 

우선 모킹을 해보자

import { func } from "../src/mockFunction";

describe("mockFunction", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("func.add 함수는 호출 됐는지 확인", () => {
    jest.spyOn(func, "add");
    const result = func.add(1, 2);
    expect(func.add).toHaveBeenCalledTimes(1);
    expect(result).toBe(3);
  });
});

 

 

위 코드를 보면 func라는 객체 안에 add라는 메서드가 있음을 유추할 수 있다.

 

jest.spyOn(func, 'add')를 통해서 func 객체 안에 있는 add 함수를 모킹을 실행함과 동시에 감시를 하게 된 것이다. 이름이 spyOn이라고 해서 '감시'라는 단어에 꽂히면 헷갈려질 수 있다.

 

----- 여기서부터는 지극히 주관적인 나의 생각 -----

지금 jest.spyOn(func, 'add') 이건 '감시'를 하고 있는 것이 아니고 모킹을 한 것이라고 이해하자.

그리고 toHaveBeenCalledTimes(1)을 통해서 함수가 1번 호출이 됐는지 검증을 하고 있다. 지금 이 코드가 '감시'로 이해하면 된다

즉 '감시'를 하기 위해서는 모킹이 되어야 하는데, 하필 그 모킹하는 함수의 이름이 jest.spyOn()인 것이다.

그럼 왜 모킹이 먼저 되어야 한다고 말을 하고 있는 걸까? 모킹을 하고 add 함수를 console.log()로 찍어보면 난생 처음 보는 함수들이 즐비할 것이다. 모킹된 함수를 호출을 했을 때 변하는 값이 있는데, 이 프로퍼티의 값들은 모킹을 하지 않으면 만들 수 없는 값들이다.

---------------------------------------------

위 내 생각을 적어놓은 이유는 "나는 모킹을 하고 싶은데 왜 자꾸 '감시' 예제만 있는 거지??"라고 생각이 든 분들도 있을 거라 생각이 든다. 실제로 내가 그랬다... 그래서 별거 아님에도 불구하고 이해하는데 시간을 꽤나 잡아먹었었다.. 후 내시간..

 

mockReturnValue, mockImplementation, mockResolvedValue

mockReturnValue

mockReturnValue는 함수가 호출될 때 항상 특정 값을 반환하도록 모킹하는 것이다.

모킹된 함수가 호출될 때 특정 값을 동기적으로 반환하도록 설정하는 것이다.

 

함수 호출 시 항상 동일한 값을 반환하고 싶을 때 사용하면 된다.

const mockFn = jest.fn();

mockFn.mockReturnValue(5);
expect(mockFn()).toBe(5);

 

 

mockImplementation

함수가 호출될 때 실행될 특정 구현을 설정하는 것이다. 즉 모킹된 함수의 구현을 직접 정의하며 반환값 뿐만 아니라 함수의 로직도 원하는 대로 설정할 수 있다.

 

함수가 특정 로직을 수행해야 하거나, 호출될 대마다 다른 동작을 해야 하는 경우 사용하면 된다.

const mockFn = jest.fn();
mockFn.mockImplementation((a, b) => a + b);

expect(mockFn(1, 2)).toBe(3);

 

 

 

mockResolvedValue

비동기 함수가 호출될 때 주어진 값을 가진 Promise를 반환하도록 설정한다. 즉 모킹된 함수가 Promise를 반환하고, 그 Promise가 성공적으로 resolve된 값을 반환하도록 설정할 수 있다

const mockFn = jest.fn();
mockFn.mockResolvedValue("resolved value");

await expect(mockFn()).resolves.toBe("resolved value");

 

 

 

catch 문으로 잡은 에러 테스트

아래와 같이 try~catch문이 있다

export function errorFunc() {
  try {
    throw new Error('error test');
  } catch (err) {
    throw new Error("error test");
  }
}

 

에러가 생겨서 catch문을 테스트해야 할 때 어떻게 할 수 있을까?

방법은 개발자마다, 팀마다 다 다를 수 있다. it 블록 안에서 모킹된 함수가 error를 반환했을 때 expect의 기대값을 에러로 검증을 해도 좋지만 지금은 try~catch문과 가장 유사한 테스트 방법을 적어보겠다

 

it("catch error test: try/catch", () => {
    try {
      throw new Error('error test')
    } catch (err) {
      expect(err).toStrictEqual(new Error("error test"));
    }
});

 

위 코드는 실행해보지 않고 그냥 생각난 대로 적은거라 잘못 적었을 확률이 조금 있다.. 그래도 어떤 느낌으로 테스트하는지 전달이 잘 됐을 거라 생각된다.

 

 

 

분명히 더 많이 있는데.. 졸려서 그런가 더이상 생각이 나지 않는다.. 생각이 더 난다면 2편으로 작성해봐야지

 

반응형