Jest에 대하여(함수, matcher, 테스트 커버리지)

박먼지·2024년 9월 15일
0
post-thumbnail

테스트 코드를 작성하는 이유

테스트 코드는 버그를 사전에 잡아줄 수 있고, 코드 품질을 유지하며, 리팩토링시 안정성을 확보하는 데 중요한 역할을 한다.

또한 잘 작성된 테스트 코드는 그 자체로 명세가 되기도 한다. 다른 개발자들이 테스트 코드를 보고 코드의 동작을 쉽게 이해할 수 있기 때문이다.

그리고 일일히 손으로 직접 테스트 하는 것이 끔찍하기 때문에 테스트 코드를 작성하는 것도 있다.

따라서 테스트 코드를 잘 작성하는 것은 중요하다는 것을 알 수 있다!

Jest란?

페이스북에서 만든 Javascript 테스팅 프레임워크이다.

jest 라이브러리 하나만 설치하면 Test RunnerTest Matcher, Test mock까지 제공해주기 때문에 테스트 환경을 쉽게 구축할 수 있다.

Test Runner: 테스트 파일을 찾아서 실행하고 성공 또는 실패 여부를 알려주는 도구를 말한다.
Test Matcher: 테스트의 기대 결과를 표현하는 구문을 의미한다. 보통 특정 조건이 맞는지 확인하는 역할을 한다.
Test mock: 실제 객체나 함수 대신 mock 객체나 함수를 사용하여 테스트하는 기법을 말한다.

또한 jest는 테스트 코드를 병렬로 실행하여 테스트 시간을 단축시킨다.

따라서 간편하고 빠른 jest를 프로젝트에 적용하기로 하였고, 제대로 사용하기 위해 jest에 대해 학습하기로 하였다.

Jest 설치 및 적용

npm i -D jest

Jest를 개발 의존성으로 설치한다.

  "scripts": {
  	"test": "jest --verbose" // 테스트 결과를 자세히 출력하는 옵션
  },

package.json파일에 test 스크립트를 jest로 설정하면 된다.

이후 테스트를 실행하고 싶으면 터미널에 npm test를 입력해서 jest를 실행하면 된다.

Jest 함수

지금까지 describe와 test만 사용했었는데, 혹시 다른 함수가 있는지 찾아보았다.

beforeEach(), afterEach()

beforeEach()각 테스트 실행 전에 공통 작업을 수행한다.
이는 테스트 환경을 항상 일정하게 유지할 수 있게 해준다.

beforeEach(() => {
  // 각 테스트 전에 실행되는 코드
  initializeSomething();
});

test('첫 번째 테스트', () => {
  // beforeEach 실행 후에 실행됨
  expect(something).toBe(true);
});

test('두 번째 테스트', () => {
  // beforeEach 실행 후에 실행됨
  expect(something).toBe(false);
});

afterEach()각 테스트 실행 후에 작업을 수행한다.
메모리 정리나 테스트 상태 초기화 등에 사용된다.

afterEach(() => {
  // 각 테스트 후에 실행되는 코드
  cleanup();
});

test('첫 번째 테스트', () => {
  expect(something).toBe(true);
  // afterEach 실행
});

test('두 번째 테스트', () => {
  expect(something).toBe(false);
  // afterEach 실행
});

beforeAll(), afterAll()

beforeAll()모든 테스트가 실행되기 전에 딱 한번 실행된다.
테스트 전반에 필요한 설정 작업을 할 때 사용된다.

beforeAll(() => {
  // 모든 테스트 전에 딱 한 번 실행되는 코드
  setupGlobalResources();
});

afterAll()모든 테스트가 끝난 후에 딱 한번 실행된다.
테스트 완류 후에 필요한 정리 작업을 수행하는데 사용된다.

afterAll(() => {
  // 모든 테스트 후에 딱 한 번 실행되는 코드
  teardownGlobalResources();
});

디버깅 용 only(), skip()

테스트 코드를 디버깅할 때 사용하는 함수이다.

테스트 함수 중 하나만 실행하고 싶은 경우 only()를 사용하면 only()가 붙은 함수만 실행된다.

test.only("run only", () => {
  // 이 테스트 함수만 실행됨
});

test("not run", () => {
  // 실행 안됨
});

반대로 한 함수를 제외하고 실행하고 싶은 경우 skip()을 사용하면 skip()이 붙은 함수를 제외하고 다른 테스트 함수들이 실행된다.

test.skip("skip", () => {
  // 이 테스트 함수는 제외됨
});

test("run", () => {
  // 실행됨
});

describe(), it(), test()

테스트 함수가 여러개 작성되어 있는 경우, describe() 함수로 묶어서 실행할 수 있다.

describe("group 1", () => {
	test("test1-1", () => {
    	// ...
    });
    
    test("test1-1", () => {
    	// ...
    });
})

test()it()은 테스트 함수를 실행하는 역할을 한다. test()it()이 다른 줄 알았는데 완전 동일하다고 한다!

함수 모킹 fn(), spyOn()

jest에서 가짜 함수(mock function)를 생성하거나 기존 함수의 동작을 감시할 때 사용한다.

  1. jest.fn()

임의의 가짜 함수를 생성할 때 사용한다.
호출 여부, 호출 횟수, 전달된 인자 등을 추적할 수 있으며, 원하는 동작을 추가하거나 반환 값을 설정할 수 있다.

const mockFunction = jest.fn();

mockFunction('arg1', 'arg2');
// 호출 여부 추적
expect(mockFunction).toHaveBeenCalled(); // ✔️ 
// 전달된 인자 추적
expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2'); // ✔️
// 호출 횟수 추적
expect(mockFunction).toHaveBeenCalledTimes(1); // ✔️

// 반환 값 설정
const mockFunction2 = jest.fn().mockReturnValue(42);
expect(mockFunction2()).toBe(42); // ✔️

//함수 구현 설정
const mockFunction3 = jest.fn((x) => x * 2);
expect(mockFunction3(2)).toBe(4); // ✔️

jest.fn()함수는 주로 콜백 함수나 직접 구현한 함수를 테스트 할 때 사용된다.

  1. jest.spyOn()
    객체의 특정 메서드를 감시(spy)하거나 모킹 할 때 사용한다.
    해당 메서드가 어떻게 호출되는지 추적할 수 있고, 필요에 따라 메서드의 동작을 대체할 수도 있다.
const myObject = {
  method: () => 'original value',
};

// myObject.method를 감시하는 spy 생성
const spy = jest.spyOn(myObject, 'method');

// 메서드 호출
myObject.method();

// 메서드가 호출되었는지 확인
expect(spy).toHaveBeenCalled(); // ✔️

실제 사용 예시

model 클래스의 create 메서드의 id가 현재 시간을 기준으로 생성되는데, 테스트를 하는 경우 create 메서드가 실행된 시간과 테스트가 실행된 시간이 맞지 않아 toEqual() 비교에 실패하는 경우가 발생하였다.

모델 클래스

export default class Model {
  ...
  create(title) {
    const newTodos = {
      title,
      completed: false,
      id: Date.now(),
    };
    this.storage.add(newTodos);
  }
}

실패한 테스트 코드

  test('create 메서드가 호출되면 store.add가 새로운 항목을 추가한다', () => {
    const title = 'test1';
    model.create(title);
    expect(store.add).toHaveBeenCalledWith({
      title,
      completed: false,
      id: Date.now(),
      // 테스트 실패
      //  -   "id": 1726398292209,
      //  +   "id": 1726398292208,
    });
  });

이를 해결하기 위해 Date.now() 메서드를 모킹하여서 Date.now()가 호출될 때마다 실제 현재 시간을 반환하는 대신 지정된 값을 반환하도록 하였다.

jest.spyOn(Date, 'now').mockReturnValue(1)

이렇게 하면 Date.now()가 호출되면 1이 반환된다.

성공한 테스트 코드

  test('create 메서드가 호출되면 store.add가 새로운 항목을 추가한다', () => {
    const title = 'test1';
    jest.spyOn(Date, 'now').mockReturnValue(1);
    model.create(title);
    expect(store.add).toHaveBeenCalledWith({
      title,
      completed: false,
      id: 1,
    });
    // 모킹된 Date.now()를 원래 상태로 복원
    jest.restoreAllMocks();
  });

모듈 모킹 mock()

함수 단위가 아닌 모듈 단위로 모킹을 하고 싶을 경우에 사용한다.

import { fetchData } from './api';
jest.mock('./api'); // './api' 모듈의 모든 함수를 모킹

Matcher

toBe()

동등 비교를 할 때 사용한다. Javascript의 === 연산자와 동일하게 동작한다.

expect(1 + 1).toBe(2); // ✔️

toEqual

객체나 배열 같은 참조형 데이터 구조를 비교할 때 사용한다.

expect({ a: 1 }).toEqual({ a: 1 });  // ✔️
expect([1, 2]).toEqual([1, 2]);  // ✔️

toBeTruthy(), toBeFalsy()

truthy하거나 falsy한 값을 확인하고 싶을 때 사용한다.

expect('hello').toBeTruthy();  // ✔️
expect(null).toBeFalsy();  // ✔️

toHaveLength(), toContain()

toHaveLength는 배열이나 문자열의 길이를 검사하고, toContain은 배열이나 문자열이 특정 요소를 포함하는지 확인할 때 사용한다.

expect([1, 2, 3]).toHaveLength(3);  // ✔️
expect([1, 2, 3]).toContain(2);  // ✔️
expect('hello world').toContain('hello');  // ✔️

toThrow()

특정 함수가 에러를 던지는지 확인할 때 사용한다. 인자로 문자열을 넘기면 예외 메시지를 비교한다.

function throwError() {
  throw new Error('Something went wrong');
}

expect(throwError).toThrow();  // ✔️
expect(throwError).toThrow('Something went wrong');  // ✔️
expect(throwError).toThrow('Success!'); // ❌

비동기 코드 테스트 resolves(), rejects()

비동기 Promise가 성공했는지, 실패했는지 확인할 때 사용한다.


// 성공적인 Promise 처리
const fetchData = () => {
  return Promise.resolve('data received');
};

test('fetches the correct data with resolves', () => {
  return expect(fetchData()).resolves.toBe('data received');
}); // ✔️

// 실패하는 Promise 처리
const fetchError = () => {
  return Promise.reject('fetch failed');
};

test('handles fetch error with rejects', () => {
  return expect(fetchError()).rejects.toBe('fetch failed');
}); // ✔️

테스트 커버리지

테스트 커버리지란?

소프트웨어 테스트에서 코드가 얼만큼 테스트되고 있는지를 나타내는 지표이다.
테스트 커버리지가 높은 소프트웨어는 버그가 발생할 확률이 적어 신뢰도 높은 소프트웨어라고 할 수 있다.

Jest 테스트 커버리지 측정하기

jest --coverage

Jest의 테스트 커버리지 유형

  • 문장(Statement) 커버리지: 코드에서 실행된 모든 개별 문장의 비율을 측정한다.

  • 분기(Branch) 커버리지: 조건문(if, else, switch)의 모든 분기가 테스트 되었는지 확인한다.

  • 함수(Function) 커버리지: 코드에서 정의된 함수나 메서드가 호출된 비율을 나타낸다.

  • 라인(Line) 커버리지: 코드에서 실행된 라인의 비율을 나타낸다.

참조
https://inpa.tistory.com/entry/JEST-%F0%9F%93%9A-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90-%EC%9C%A0%EC%9A%A9%ED%95%9C-%ED%95%A8%EC%88%98-only-skip-describe-it
https://www.daleseo.com/jest-basic/

profile
개발괴발

0개의 댓글