IT/기초 지식

[Jest] Jest로 유닛 테스트 작성하기 (기본)

개발자 두더지 2023. 1. 17. 22:21
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

Jest란?


 Jest이란 Facebook에서 개발하고 있는 OSS의 JavaScript 테스트 워크 프레임이다. 유명한 프로젝트로는  Babel, TypeScript, Node, React, Angular, Vue 등에서 이용되고 있다.

 Jest의 Github 리포지토리는 다음의 URL을 참고해주시길 바란다.

https://github.com/facebook/jest

 

 

Jest 설치 및 기본 셋팅


 npm의 환경이 준비되어 있다면 다음의 커맨드로 설치할 수 있다.

$ npm init
$ npm install --save-dev jest

 그리고 package.json에 jest를 추가한다.

{
  "name": "fizzbuzz_tester",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^24.1.0",
  }
}

 

 

사양 정하기


 코드를 적기 전에 먼저 사양을 결정하자. 이번에는 유명한 FizzBuzz를 만들어려고 한다. FizzBuzz는 다음과 같다.

  •  1부터 100까지의 정수를 1부터 순서대로 출력한다.
  • 정수가 3의 배수인 경우, 정수 대신에 Fizz가 출력된다.
  • 정수가 5의 배수인 경우, 정수 대신에 Buzz가 출력된다.
  • 정수가 15의 배수인 경우, 정수 대신에 FizzBuzz가 출력된다.

 

 

코드 작성


그럼 코드를 작성해보자.

// app.js

const app = () => {
  for (let i = 1; i <= 100; i++) {
    if (i % 3 == 0 && i % 5 == 0) {
      console.log('FizzBuzz');
      continue;
    }

    if (i % 3 == 0) {
      console.log('Fizz');
      continue;
    }

    if (i % 5 == 0) {
      console.log('Buzz');
      continue;
    }

    console.log(i);
  }
};

module.exports = app;

 이 파일을 실행해보면 사양대로 움직이고 있음을 알 수 있다. 그럼 테스트는 어떻게 하면 좋을까? 답은 "먼저 코드를 고친다"이다. 이대로는 테스트가 복잡한 구조가 되어버리기 때문이다.

 유닛 테스트의 기본은 I/O와 로직을 분리하는 것이므로, 분리하여 코드를 수정해야한다.

 

 

코드 수정하기


 위 코드를 포함한 I/O라고 한다면, 물론 console.log()이다. 이것을 분리해보자.

// app.js

const app = () => {
  for (let i = 1; i <= 100; i++) {
    console.log(toFizzBuzz(i));
  }
};

const toFizzBuzz = num => {
  if (num % 3 == 0 && num % 5 == 0) return 'FizzBuzz';
  if (num % 3 == 0) return 'Fizz';
  if (num % 5 == 0) return 'Buzz';
  return num;
};

module.exports = app;

 toFizzBuzz에 FizzBuzz 룰을 저장하고, 처리의 흐름과 결과 출력은 app에 맡긴다. 이것으로 I/O는 분리됐지만, 실제로 아직 테스트는 할 수 없는 상태이다.

 위 코드에서는 모듈로서 개방되어 있는 것은 app뿐이다. 로직의 주체가 되는 toFizzBuzz를 테스트하기 위해서는 이것도 모듈로 개방할 필요가 있지만, 사양에 포함되어 있지 않은 불필요한 것을 노출시킬 필요는 없을 것다. 

 그럼 어떻게 하면 좋을까? 방법은 몇 가지가 있지만, 이번에는 toFizzBuzz를 다른 파일로 나눈다.

//app.js

const toFizzBuzz = require('./modules/fizzbuzz');

const app = () => {
  for (let i = 1; i <= 100; i++) {
    console.log(toFizzBuzz(i));
  }
};

module.exports = app;
//modules/fizzbuzz.js

const toFizzBuzz = num => {
  if (isFizz(num) && isBuzz(num)) return 'FizzBuzz';
  if (isFizz(num)) return 'Fizz';
  if (isBuzz(num)) return 'Buzz';
  return num;
};

const isFizz = num => {
  return num % 3 == 0;
};

const isBuzz = num => {
  return num % 5 == 0;
};

module.exports = toFizzBuzz;

 이제 테스트가 쉬워질 것이다. app.js에 대해서는 console.log와 같이 이미 품질이 보증된 것은 테스트가 불필요하므로, 테스트해야말 부분은 for문을 실행하고 있는 부분뿐이다. 이것은 모크를 이용하여 테스트해야만 하지만, 글이 길어지므로 이번에는 생략하고 fizzbuzz.js의 시점에서 코드를 짜보자.

 

 

테스트


 그럼 테스트 코드를 써보자. 블랙 박스 테스트에서는 동지 분할이나 경계치 분석에 대해서 생각해 볼 필요가 있다고들 말하는데, 이번은 화이트 박스 테스트를 할 것이므로 필요없다. 화이트 박스 테스트는 코드 작성자가 구현할 것이므로, 그 코드가 어떤 동작을 해야만 하는지 보증되어 있기 때문이다.

 화이트 박스 테스트에서의 코드 동작에 대해서는 플로우 차드를 작성하면 알기 쉽다. 플로우 차드를 작성하기 위해서는 먼저 이 fizzbuzz.js에 필요한 사양을 생각해 보자.

  • 1개의 인수 num을 받는다
  • 인수가 정수가 아닌 경우, 에러를 출력한다.
  • 정수가 1미만인 경우, 에러를 출력한다.
  • 정수가 15 배수인 경우, FizzBuzz를 반환환다.
  • 정수가 3의 배수인 경우, Fizz를 반환한다.
  • 정수가 5의 배수인 경우, Buzz를 반환한다.
  • 정수가 3의 배수도, 5의 배수도 아닌 경우, 그대로 숫자를 반환한다.

 사양에 따라 플로우 차트를 쓴다면 다음과 같다.

 그럼 플로우 차트를 만족하도록 테스트 코드를 작성해보자.

// modules/fizzbuzz.test.js
const toFizzBuzz = require('./fizzbuzz');

test('fizzbuzz의 유닛테스트', () => {
  expect(() => toFizzBuzz('test')).toThrow(RangeError);

  expect(() => toFizzBuzz(-1)).toThrow(RangeError);
  expect(() => toFizzBuzz(0)).toThrow(RangeError);

  expect(toFizzBuzz(1)).toBe(1);
  expect(toFizzBuzz(3)).toBe('Fizz');
  expect(toFizzBuzz(5)).toBe('Buzz');
  expect(toFizzBuzz(15)).toBe('FizzBuzz');
});

 그럼 실행해보자. package.json에 test커맨드를 등록해놨으므로 그것을 사용한다.

$ npm run test

 FAIL  middleware/fizzbuzz.test.js
  ✕ fizzbuzz의 유닛테스트 (4ms)

  ● fizzbuzz의 유닛테스트

    expect(received).toThrow(expected)

    Expected name: "RangeError"

    Received function did not throw

      2 |
      3 | test('fizzbuzzのユニットテスト', () => {
    > 4 |   expect(() => toFizzBuzz('test')).toThrow(RangeError);
        |                                    ^

 어딘가 걸렸다. 당연하다. 인수의 데이터 형 제한 처리를 쓰지 않았기 때문이다. 이와 같이, 사양의 설정과 테스트 코드를 제대로 입력해두면 코드의 동작을 보증해주므로, 무심코 할 수 있는 실수를 줄여준다.  테스트 코드를 쓰는 커다란 메리트 중의 하나이다.

 테스트의 효과를 확인했으므로, 코드를 테스트에 통과하도록 수정해보자.

// middleware/fizzbuzz.js 

const toFizzBuzz = num => {
  if (outOfRange(num))
    throw new RangeError('인수가 올바르지 않습니다. 1이상의 정수만 인수로 지정가능합니다.');

  if (isFizz(num) && isBuzz(num)) return 'FizzBuzz';
  if (isFizz(num)) return 'Fizz';
  if (isBuzz(num)) return 'Buzz';
  return num;
};

const outOfRange = num => {
  if (!(typeof num === 'number')) return true;
  if (Math.round(num) != num) return true;
  if (num < 1) return true;

  return false;
};

const isFizz = num => {
  return num % 3 == 0;
};

const isBuzz = num => {
  return num % 5 == 0;
};

module.exports = toFizzBuzz;

 수정했으면 다시 커맨드로 테스트를 실행해보자.

 PASS  middleware/fizzbuzz.test.js
  √ fizzbuzz의 유닛 테스트 (15ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.079s
Ran all test suites.

 통과됐다. 그러나 테스트를 무사히 통과했다고 해서 반드시 버그가 일어나지 않는다는 것은 아니다. 테스트는 어디까지나 테스트한 범위에서 문제가 없는 것이기 때문이다.

 

 

기타 tip


 커맨드를 입력할 때 --coverage 옵션을 추가하면 테스트의 커버리지를 자동으로 조사해여 결과를 출력해준다. 작업 디렉토리에 coverage이라는 디렉토리가 생성되어, 그 안에 Iconv-report/index.html를 열어보면 상세한 내용이 확인 가능하다.

$ npm run test -- --coverage

 커버리지의 상세내용이나 테스트가 부족한 부분을 한 눈에 알게 된다. 


참고자료

https://zenn.dev/ganezasan/books/78676684ccdeb090f7b8/viewer/chapter_2

https://qiita.com/jintz/items/61af86a12b53b24ef121

728x90