JavaScript 3.5

4 minute read

테스트 자동화와 Mocha

테스트는 왜 해야 하는가?

  • 코드를 수동으로 재실행 하면서 테스트를 하면 무언가를 놓치기 쉽다.
  • 테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데, 이때 실행 결과와 기대 결과를 비교할 수 있다.

Behavior Driven Development

  • BDD
  • 테스트, 문서, 예시를 한데 모아놓은 개념.

명세서 specification, spec

  • 스펙은 세 가지 주요 구성 요소로 이루어진다.
    • Describe(“title”, function() {…})
      • 구현하고자 하는 기능에 대한 설명, it 블록을 모아주기도 한다.
    • it(“유스케이스 설명”, function() {…})
      • 유스케이스에 대한 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적는다.
      • 유스케이스 테스트 함수가 두번째 인수로 들어간다.
    • assert.*(value1, value2)
      • 기능을 제대로 구현해서 예상대로 동작되는지 확인한다.
  • 거듭제곱 함수와 명세서
describe("pow", function() {

  it("주어진 숫자의 n 제곱", function() {
    assert.equal(pow(2, 3), 8);
  });

});
  • x를 n번 곱해주는 함수, pow(x, n)를 구현하고 있다고 가정. (단, n은 자연수이고, 조건 n≥0을 만족해야 한다.)
  • Describe 에서는 pow가 어떤 동작을 하는지 설명하며, it에서는 유스케이스 설명이 들어간다. assert.equal은 pow가 예상한 대로 동작한지 확인해준다.

개발 순서

  • 개발 순서는 다음과 같다.
    • 명세서 초안을 작성한다.
    • 명세서 초안을 보고 코드를 작성한다.
    • 코드가 작동하는지 확인하기 위해 Mocha라 불리는 테스트 프레임워크를 사용해 명세서를 실행한다.
      • 이때 코드가 잘못 작성되었다면 에러가 출력된다.
    • 모든 테스트를 통과하는 코드 초안이 완성된다.
    • 명세서에 지금까지 고려하지 않았던 유스케이스 몇 가지를 추가한다.
      • 테스트가 실패한다.
    • 테스트를 통과할 때까지 코드를 수정한다.
    • 기능이 완성될 때까지 테스트 프레임워크를 사용해 명세서를 실행하고 코드를 수정하는 것을 반복한다.

스펙 실행하기

  • 3개의 라이브러리를 사용해서 테스트를 진행한다.
  • Mocha – 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공한다.
  • Chai – 다양한 assertion을 제공해 주는 라이브러리. assert.equal 정도만 사용해 본다.
  • Sinon – 함수의 정보를 캐내는 데 사용되는 라이브러리로, 내장 함수 등을 모방한다. 본 챕터에선 사용하지 않고, 다른 챕터에서 실제로 사용해본다.
// pow 스펙, 라이브러리가 들어있다.

<!DOCTYPE html>
<html>
<head>
  <!-- 결과 출력에 사용되는 mocha css를 불러옵니다. -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
  <!-- Mocha 프레임워크 코드를 불러옵니다. -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
  <script>
    mocha.setup('bdd'); // 기본 셋업
  </script>
  <!-- chai를 불러옵니다 -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
  <script>
    // chai의 다양한 기능 중, assert를 전역에 선언합니다.
    let assert = chai.assert;
  </script>
</head>

<body>

  <script>
    function pow(x, n) {
      /* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
    }
  </script>

  <!-- 테스트(describe, it...)가 있는 스크립트를 불러옵니다. -->
  <script src="test.js"></script>

  <!-- 테스트 결과를 id가 "mocha"인 요소에 출력하도록 합니다.-->
  <div id="mocha"></div>

  <!-- 테스트를 실행합니다! -->
  <script>
    mocha.run();
  </script>
</body>

</html>
  • <head> : 테스트에 필요한 서드파티 라이브러리와 스타일을 불러옴.
  • <script> : 테스트할 함수(pow)의 코드가 들어감.
  • 테스트 : describe(“pow”, …)를 외부 스크립트(test.js`)에서 불러옴.
  • HTML 요소 <div id="mocha"> : Mocha 실행 결과가 출력됨.
  • mocha.run() : 테스트를 실행시켜주는 명령어.

  • 결과 : 실패.
    • Pow(2,3)가 8이 아닌 undefined를 반환하기 때문에.

코드 초안

  • 테스트 통과만을 목적으로 코드를 간단하게 작성한다.
function pow(x, n) {
  return 8; // 속임수를 써봤습니다. :)
}

스펙 개선하기

  • 스펙에 테스트를 추가하는 방법은 아래와 같이 두 가지가 있다.
  • 기존 it 블록에 assert를 하나 더 추가하기.
describe("pow", function() {

  it("주어진 숫자의 n 제곱", function() {
    assert.equal(pow(2, 3), 8);
    assert.equal(pow(3, 4), 81);
  });

});
  • 테스트 하나 더 추가하기(it 블록 하나 더 추가하기)
describe("pow", function() {

  it("2를 세 번 곱하면 8입니다.", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3을 네 번 곱하면 81입니다.", function() {
    assert.equal(pow(3, 4), 81);
  });

});
  • assert에서 에러가 발생하면 it 블록은 즉시 종료된다.
    • 따라서 기존 it 블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두번째 assert의 결과를 알 수 없다.
    • 두번째 방법처럼 it 블록을 하나 더 추가해 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있기 때문에 두번째 방법을 추천한다.
  • 여기에 더하여 테스트 하나에선 한 가지만 확인하는 것이 좋다.

코드 개선하기

  • 두번째 테스트도 통과할 수 있도록 코드를 개선한다.
function pow(x, n) {
  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

// 테스트
describe("pow", function() {

  function makeTest(x) {
    let expected = x * x * x;
    it(`${x}을/를 세 번 곱하면 ${expected}입니다.`, function() {
      assert.equal(pow(x, 3), expected);
    });
  }

  for (let x = 1; x <= 5; x++) {
    makeTest(x);
  }

});

중첩 describe

  • 테스트를 더 추가한다.
describe("pow", function() {

  describe("x를 세 번 곱합니다.", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x}을/를 세 번 곱하면 ${expected}입니다.`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }

  });

  // describe와 it을 사용해 이 아래에 더 많은 테스트를 추가할 수 있습니다.
});

스펙 확장하기

  • n이 조건에 맞지 않을 때 NaN을 반환하는지 아닌지 검사하는 테스트를 추가한다.
describe("pow", function() {

  // ...

  it("n이 음수일 때 결과는 NaN입니다.", function() {
    assert.isNaN(pow(2, -1));
  });

  it("n이 정수가 아닐 때 결과는 NaN입니다.", function() {
    assert.isNaN(pow(2, 1.5));
  });

});

// 테스트 후 코드 추가
function pow(x, n) {
  if (n < 0) return NaN;
  if (Math.round(n) != n) return NaN;

  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

요약

  • 스펙의 용도는 세가지이다.
    • 테스트 : 함수가 의도하는 동작을 제대로 수행하고 있는지 보장.
    • 문서 : 함수가 어떤 동작을 수행하고 있는지 설명.
    • 예시 : 실제 동작하는 예시를 이용해 함수를 어떻게 사용할 수 있는지 알려줌.
  • 스펙이 있기 때문에 개발자는 안전하게 함수를 개선하거나 변경할 수 있다.
  • 코드가 바뀌어도 기존에 구현된 기능에 영향을 주지 않게 하는 것이 중요한데 테스트 자동화는 이것이 가능하게 만들어준다.
  • 잘 테스트 된 코드는 더 나은 아키텍쳐를 만든다.

Categories:

Updated: