TDD와 Jest 알아보기 (feat. 우테코 6기)

김학재·2023년 10월 31일
0

자바스크립트

목록 보기
17/17

평소 프로젝트를 진행하면서 테스트 코드를 작성해보자 하는 마음은 있었으나 어떻게 시작해야할지 감을 잡지 못해 시작하지 못하고 있었다.

그러던 와중, 현재 진행하고 있는 우아한테크코스에서 jest를 활용해 테스트 코드를 작성할 기회가 생겨 jest를 알아보고 어떻게 테스트를 진행하는지 알아보려고 한다!

더불어 TDD가 왜 중요한지, 기업들은 왜 테스트 작성 경험이 있는 개발자를 선호하는지에 대해서도 알아보자


TDD

TDD cycle

TDD는 무엇인가?

TDD(Test-Driven Development), 우리말로 테스트 주도 개발은 소프트웨어가 완전히 개발되기 전에, 모든 테스트 케이스에 대해 반복적으로 테스트함으로써 소프트웨어 요구사항을 검증하는 소프트웨어 개발 프로세스 중 하나이다.

왜 TDD인가?

TDD는 누군가 새로 창시하거나 무에서 새로 생겨난 개념이 아니다. 이 개념을 개발 혹은 "재발견"한 것으로 인정되는 Kent Beck에 따르면 TDD를 하는 이유는,

Why would a software engineer take on the additional work of writing automated tests?
Why would a software engineer work in tiny little steps when his or her mind is capable of great soaring swoops of design? Courage.

왜 소프트웨어 엔지니어는 자동화된 테스트를 작성하기 위해 추가 작업을 해야 하는가?
왜 소프트웨어 엔지니어는 더 솟구치는 디자인에 대한 열정으로 가득함에도 왜 더 작은 단계의 일을 해야 하는가? 바로 용기이다.

즉, 테스트를 작성함으로써 소프트웨어의 단순한 설계를 장려하고 자신감을 불어넣어주기 때문이다. 그는 TDD를 통해 프로그래밍에 있어 공포를 줄여준다고 기술한다. 또한, TDD는 "의사소통을 하고 싶지 않아 하는", "소통을 멀리하는" 공포를 줄이는 용기를 갖춰 더 나은 프로그래밍 경험을 가능하도록 한다고 기술한다.

TDD가 무엇이고 왜 이것이 중요한지 알았으니 이제 실제로 TDD를 경험해보자!

Jest

Jest는 대표적인 자바스크립트 테스트 프레임워크이다. Jest는 2011년 페이스북의 채팅 기능이 자바스크립트로 재작성될 당시 만들어졌다. 나날이 증가해가는 복잡성은 빠른 TDD 개발 사이클을 필요로 했고 이것이 Jest의 탄생 계기가 되었다.

Jest는 다른 프레임워크들과 비교해도 월등히 높은 인기를 구가하고 있다.
Jest - npm trends with other test libraries

사용법

Jest를 설치하고 테스트 스크립트를 실행하면

// package.json
"scripts": {
  "test": "jest"
}

Jest는 test.js로 끝나거나, __test__디렉토리 안에 있는 파일을 테스트 파일로 인식해 테스트를 수행한다.

기본적인 문법을 살펴보면

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

expect 내부 함수의 적용 결과가 toBe 내부의 값과 같은지 검증하는 테스트를 실행할 수 있다.
여기서 toBe와 같이 값을 테스트하는 방법을 Matcher라고 부르며, toEqual(), toThrow()와 같은 다양한 Matcher를 제공한다.

이외에도 Jest는 비동기 코드, Mock Function 등 다양한 기능들을 제공한다. 이번 글에서는 우테코 6기 1주차에 적용된 테스트 코드를 통해 Jest의 다양한 기능들을 알아보자

적용 예시 (우테코)

ApplicationTest.js

Mock Function

const mockQuestions = (inputs) => {
  MissionUtils.Console.readLineAsync = jest.fn();

  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();
    return Promise.resolve(input);
  });
};

const mockRandoms = (numbers) => {
  MissionUtils.Random.pickNumberInRange = jest.fn();
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
};

const getLogSpy = () => {
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  logSpy.mockClear();
  return logSpy;
};

Mock functions은 단순히 출력값을 테스트하는 것을 넘어, 다른 코드로부터 간접적으로 실행되는 함수의 동작을 조작할 수 있으므로 "spies"라고도 불린다. jest.fn()을 사용해 mock function을 생성할 수 있다.

위 코드를 살펴보면

  • mockQuestions
    사용자의 입력을 받는 MissionUtils.Console.readLineAsync을 mock function으로 만들고, mockImplementation을 통해 주어진 입력 값을 비동기로 반환하게끔 동작하게 한다.

  • mockRandoms
    랜덤 숫자를 반환하는 MissionUtils.Random.pickNumberInRange를 mock function으로 만들고, numbers 배열을 받아 MissionUtils.Random.pickNumberInRange가 실행될 때 numbers의 숫자가 차례대로 반환되도록 한다.

  • getLogSpy
    spyOn(object, methodName)은 mock function을 만들고, object[methodName]의 호출을 추적한다.
    위 코드에서는 MissionUtils.Console 객체의 print의 호출을 추적한다.

describe

describe(name, fn)은 연관된 테스트를 묶어 하나의 블록을 생성한다.

describe("숫자 야구 게임", () => {
  test("게임 종료 후 재시작", () => {
    ...
  });
    
  test("예외 테스트", () => {
    ...
  });
});

describe.each를 사용해 다른 데이터들에 대해 동일한 테스트 스위트를 수행할 수 있고, describe.skip 키워드를 사용해 테스트를 생략하는 등 동작을 지정할 수 있다.

test

test(name, fn, timeout)메소드는 말 그대로 테스트를 수행하는 메소드이다. test대신, it을 사용하기도 한다.

첫번째 인자로는 테스트의 이름을, 두번째 인자로는 수행하고자 하는 동작을 넣는다. 세번째 인자(optional)에는 timeout(기본값 5초)을 넣는다.

test로부터 promise가 return된다면, Jest는 테스트를 끝내기 전에 promise가 완료되기를 기다린다. async/await을 사용해 테스트를 수행할 수 있다.

expect

expect와 다양한 matcher를 사용해 다양한 값을 다양한 조건으로 검증할 수 있다.
대표적인 matcher만 살펴보면

  • toBe(value)
    인스턴스의 원시 값 혹은 참조값을 검증하는 데 사용한다. Object.is를 사용해 값을 비교한다. 깊은 비교가 일어나기 때문에, 만약 테스트가 실패한다면, 비교 동작을 expect내부로 옮겨야 할 수도 있다.
  • toHaveBeenCalled()
    mock function이 호출되었는지를 검증한다.

위 개념들을 바탕으로 아래 테스트 코드를 살펴보면,

describe("숫자 야구 게임", () => {
  test("게임 종료 후 재시작", async () => {
    // given
    const randoms = [1, 3, 5, 5, 8, 9];
    const answers = ["246", "135", "1", "597", "589", "2"];
    const logSpy = getLogSpy();
    const messages = ["낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료"];

    mockRandoms(randoms);
    mockQuestions(answers);

    // when
    const app = new App();
    await expect(app.play()).resolves.not.toThrow();

    // then
    messages.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });
});
- describe을 통해 숫자 야구 게임 관련 테스트를 묶는다
- test를 통해 게임 종료 후 재시작 기능 테스트를 진행한다
- logSpy를 활용해 출력값을 검증한다
- mockRandoms를 활용해 랜덤 값을 생성한다. 즉, 첫번째로 Random.pickNumberInRange가 실행되면 1, 그 다음은 3.. 이런 식으로 반환한다
- play()가 실행되면 야구 게임을 진행한다
- 첫번째 랜덤값인 "135"에 대해, 사용자의 추측인 "246"은 "낫싱", "135"는 "3스트라이크" 결과를 갖는다
- 이후 사용자가 "1"을 입력하면 다시 랜덤값을 생성하고 "589"에 대해 검증을 진행한다

이외에도, Jest는 비동기 동작에 대해서도 테스트를 진행할 수 있다.

결론

TDD가 무엇이고 왜 중요한지를 알아보고 대표적인 test framework인 Jest를 알아보았다.
개발에 있어 소프트웨어의 유지보수성을 높이고 이를 바탕으로 자신있게 일할 수 있기에 테스트 경험이 있는 개발자를 선호하는 것은 당연해보인다.
Jest의 간단한 활용법에 대해서도 익혔으니 우테코를 비롯해 다양한 곳에서 활용해보자!


참고 자료

profile
YOU ARE BREATHTAKING

0개의 댓글