[Modern JS] 3.5 테스트 자동화와 Mocha

LeeHanna·2021년 10월 11일
0

Modern JS

목록 보기
2/2
post-thumbnail

테스트 자동화는 현업에서도 빈번히 사용된다. 테스트 자동화는 왜 해야하는 것이며 어떻게 하는 것인지에 대해서 알아보자.

🤔 테스트는 왜 해야 할까?

함수를 하나 만들고 있다고 가정했을 때, 보통 매개변수 - 결과 관계를 중심으로 어떻게 코드를 작성할 지 구상한다. 개발 중에는 콘솔 창등을 이용해 원하는 기능이 잘 작동하는지 계속해서 확인하곤 한다. 원하는 기능이 작동할때 까지 수정 → 확인 → 수정 → 확인 이 과정을 반복하는데, 이렇게 수동으로 코드를 재실행 하는 것은 상당히 불완전하다.

코드를 수동으로 재실행 하면서 테스트를 하면 무언가를 놓치기 쉽기 때문이다.

예를 들어, 함수 f를 구현할 때 코드를 작성하고 f(1)이 제대로 동작하는지 확인한다. f(1)은 제대로 동작하지만 f(2)는 제대로 동작하지 않는다. 그렇다면 f(2)를 제대로 동작하도록 코드를 수정한다. 수정 후에 f(2)가 제대로 동작한다. 하지만 여기서 끝이 아니다. 다시 한 번 f(1)도 제대로 동작하는지 다시 확인해주어야 한다. 이렇게 테스트를 수동으로 하면 에러가 발생할 여지를 남기게 된다.

따라서, 테스트를 하는 이유는 다음과 같다.

💡 개발자는 어떤 기능을 구현할 때 머릿속에 수많은 유스 케이스를 생각하며 코드를 작성하는데, 코드 변경시 모든 유스케이스를 상기하면서 코드를 수정하는 것은 불가능하며 하나를 고치면 또 다른 문제가 튀어나오는 오류가 발생한다. 그렇기 때문에 테스트를 자동화 하는 것이다.

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

Behavior Driven Development (BDD) 방법론

테스트 자동화 방법론에는 Behavior Driven Development (BDD)라 불리는 방법론이 있다.

BDD는 테스트(Test), 문서(Documentation), 예시(Example)를 한데 모아놓은 개념이다.

실제 개발 사례를 이용해 BDD가 무엇인지 알아보자.

거듭제곱 함수와 명세서

xn번 곱해주는 함수 pow(x, n) 을 구현하고 있다고 가정해보자. (단, n 은 자연수이고, 조건 n>=0 을 만족해야 한다.)

사실 자바스크립트에는 거듭제곱 연산자 ** 가 있지만, 함수를 직접 구현하면서 BDD를 직접 적용해보자.

본격적으로 코드를 작성하기 전에 먼저 해야할 것은 코드가 무슨 일을 하는지 상상한 후 이를 자연어로 표현하는 것이다.

이때, 만들어진 산출물을 BDD에선 명세서(specification) 또는 짧게 줄여 스펙(spec)이라고 부른다. 명세서엔 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.

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

});

스펙은 세 가지 주요 구성 요소로 이루어져있다.

describe("title", function() { ... })

→ 구현하고자 하는 기능에 대한 설명이 들어간다. 이번 예시에서는 pow 가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.

it("유스케이스 설명", function() { ... })

it 의 첫 번째 인수엔 특정 유스케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어준다. 두 번째 인수엔 유스 케이스의 테스트 함수가 들어간다.

assert.equal(value1, value2)

→ 기능을 제대로 구현했다면 it 블록 내의 코드 assert.equal(value1, value2) 이 에러 없이 실행된다.

함수 assert.*pow 가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal 이 사용되었는데, 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환하는 함수이다. 예시에서는 pow(2, 3) 의 결괏값과 8 을 비교한다.

명세서는 실행 가능하다. 명세서를 실행하면 it 블록 안의 테스트가 실행된다.

개발 순서

실제로 개발을 하게되면 아래와 같은 순서로 개발이 진행된다.

  1. 명세서 초안을 작성한다. 초안에는 기본적인 테스트도 포함된다.
  2. 명세서 초안을 보고 코드를 작성한다.
  3. 코드가 작동하는지 확인하기 위해 Mocha 라 불리는 테스트 프레임워크를 사용해 명세서를 실행한다. 이때, 코드가 잘못 작성되었다면 에러가 출력된다. 개발자는 테스트를 통과해 에러가 더는 출력되지 않을 때까지 코드를 수정한다.
  4. 모든 테스트를 통과하는 코드 초안이 완성된다.
  5. 명세서에 지금까지는 고려하지 않았던 유스케이스 몇 가지를 추가한다. 테스트가 실패하기 시작한다.
  6. 3단계로 돌아가 테스트를 모두 통과할 때까지 코드를 수정한다
  7. 기능이 완성될 때까지 3~6 단계를 반복한다.

위와 같은 방법은 반복적인(iterative) 성격을 지닌다. 이 과정을 계속 하다보면 끝으로는 완전히 동작하는 코드와 테스트 둘 다를 확보하게 된다.

이제 실제 사례에 위 개발 순서를 적용해보자.

함수 pow 의 스펙 초안은 이미 위에서 작성했으므로, 첫 단계는 끝난 상태이다.

스펙 실행하기

해당 튜토리얼에선 총 3개의 라이브러리를 사용해 테스트를 진행한다. 각 라이브러리에 대한 설명은 아래와 같다.

  • Mocha - 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공한다.
  • Chai - 다양한 assertion을 제공해 주는 라이브러리입니다. 우리 예시에선 assert.equal 정도만 사용해 볼 예정이다.
  • Sinon - 함수의 정보를 캐내는 데 사용되는 라이브러리로 내장 함수 등을 모방한다.

세 라이브러리 모두 브라우저나 서버 사이드 환경을 가리지 않고 사용 가능하다. 여기에서는 브라우저 환경을 가정하고 사용해본다.

아래 HTML 페이지에는 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="3.5 test.js"></script>

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

    <!-- 테스트 실행 -->
    <script>
      mocha.run();
    </script>
  </body>
</html>

위 페이지는 다섯 부분으로 나눌 수 있다.

  1. <head> - 테스트에 필요한 서드파티 라이브러리와 스타일을 불러온다.
  2. <script> - 테스트할 함수 pow 의 코드가 들어간다.
  3. 테스트 - describe("pow", ...) 를 외부 스크립트(test.js)에서 불러온다.
  4. HTML 요소 <div id="mocha"> - Mocha 실행 결과가 출력된다.
  5. mocha.run() - 테스트를 실행시켜주는 명령어이다.

이대로 스펙을 실행시키면 다음과 같은 에러가 발생한다.

지금은 함수 pow 본문에 아무런 코드가 없기 때문에 pow(2,3)undefined를 반환하여 테스트가 실패할 수 밖에 없다.

코드 초안

오로지 테스트 통과만을 목적으로 코드를 간단하게 작성해보자.

function pow(x,n) {
	return 8; // 속임수 사용
}

이제 스펙을 실행해도 에러가 발생하지 않는다.

스펙 개선하기

위에서는 속임수를 사용해 코드를 작성하여 테스트를 모두 통과한다.

하지만 함수가 제 역할을 못하기 때문에 더 많은 유스케이스를 추가해보자.

pow(3,4) = 81 을 만족하는지 확인하는 테스트를 추가해보자.

스펙에 테스트를 추가하는 방법은 두가지가 있다.

  1. 기존 it 블록에 assert 를 하나 더 추가하기
describe("pow", function() {

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

});
  1. 테스트를 하나 더 추가하기 (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 블록을 하나 더 추가해 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있기 때문에 두 번째 방법을 추천한다.

여기에 더하여 테스트를 추가할 땐 다음 규칙도 따르는 것이 좋다.

테스트 하나에선 한 가지만 확인하기
테스트 하나에서 연관이 없는 사항 두개를 점검하고 있다면 분리하는 것이 좋다.

아래는 2번 방법을 사용해 테스트를 추가해 본 결과이다.

이번에는 두 번째 테스트만 실패했다. assert 에서 함수 리턴 값이 81 이 될 것이라 기대했는데 함수는 항상 8 을 반환하고 있기 때문에 당연히 테스트를 통과할 수 없다.

코드 개선하기

두 번째 테스트도 통과할 수 있도록 코드를 개선해보자. 속임수를 사용하지 않고 실제로 우리가 구현하고자 한 거듭제곱 기능을 생각하며 코드를 작성해 본다.

function pow(x, n) {
  let result = 1;

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

  return result;
}

함수가 제대로 작동하는지 확인하기 위해 더 많은 값을 테스트해본다. 수동으로 여러 개의 it 블록을 만드는 대신 for 문을 사용해 자동으로 it 블록을 만들어본다.

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을 사용해 이 아래에 더 많은 테스트를 추가할 수 있다.
});

위 코드에서 makeTest 는 오직 for 문에서만 사용되고, 다른 데서는 사용되지 않기 때문에 위와 같이 묶어두었다. 위 스펙에서 makeTestfor 문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.

이렇게 중첩 describe 를 사용하면 그룹을 만들 수 있다.

중첩 describe 는 새로운 테스트 '하위 그룹(subgroup)'을 정의할 때 사용된다. 이렇게 새로 정의된 테스트 하위 그룹은 테스트 결과 보고서에 들여쓰기 된 상태로 출력된다.

만약 나중에 자체 헬퍼 함수를 가진 itdescribe 를 최상위 레벨에 추가한다면 이들 헬퍼 함수에선 makeTest에 접근할 수 없다.

before/after와 beforeEach/afterEach

함수 before는 (전체) 테스트가 실행되기 전에 실행되고, 함수 after는 (전체) 테스트가 실행된 후에 실행된다. 함수 beforeEach 는 매 it 이 실행되기 전에 실행되고, 함수 afterEach 는 매 it 이 실행된 후에 실행된다.

describe("test", function() {

  before(() => alert("테스트를 시작합니다 - 테스트가 시작되기 전"));
  after(() => alert("테스트를 종료합니다 - 테스트가 종료된 후"));

  beforeEach(() => alert("단일 테스트를 시작합니다 - 각 테스트 시작 전"));
  afterEach(() => alert("단일 테스트를 종료합니다 - 각 테스트 종료 후"));

  it('test 1', () => alert(1));
  it('test 2', () => alert(2));

});

실행 순서는 다음과 같다.

테스트를 시작합니다 - 테스트가 시작되기 전          (before)
단일 테스트를 시작합니다 - 각 테스트 시작 전         (beforeEach)
1
단일 테스트를 종료합니다 - 각 테스트 종료 후         (afterEach)
단일 테스트를 시작합니다 - 각 테스트 시작 전         (beforeEach)
2
단일 테스트를 종료합니다 - 각 테스트 종료 후         (afterEach)
테스트를 종료합니다 - 테스트가 종료된 후            (after)

beforeEach/afterEachbefore/after 는 대개 초기화 용도로 사용된다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때 (또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업들이 있으면 이들을 이용 할 수 있다.

스펙 확장하기

첫 번째 반복에선 함수 pow 의 기본적인 기능을 구현해보았다. 또 다른 반복을 돌며 기능을 개선해보자.

앞서 정의했듯이 함수 pow(x,n) 의 매개변수 n 은 양의 정수여야 한다.

자바스크립트에서는 수학 관련 연산을 수행하다 에러가 발생하면 NaN 을 반환한다.

함수 pown 이 조건에 맞지 않을 때 함수가 NaN을 반환하는지 아닌지를 검사해주는 테스트를 추가해보자.

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);
    }
  });

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

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

스펙을 실행하면 다음과 같은 결과가 출력된다.

기존엔 n이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기서 나타난다. 실패할 수 밖에 없는 테스트를 추가하고 테스트를 통과할 수 있게 코드를 개선하는 것이다.

새롭게 추가한 테스트를 통과할 수 있도록 pow에 코드를 추가해보자.

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;
}

이제 에러 없이 테스트를 모두 통과하는 것을 확인할 수 있다.

다양한 assertion

Chai는 다양한 assertion을 지원하는데 그 중 몇 가지는 아래와 같은 용도로 사용된다.

  • assert.equal(value1, value2) – value1과 value2의 동등성을 확인한다. (value1 == value2)
  • assert.strictEqual(value1, value2) – value1과 value2의 일치성을 확인한다. (value1 === value2)
  • assert.notEqualassert.notStrictEqual – 비 동등성, 비 일치성을 확인한다.
  • assert.isTrue(value) – value가 true인지 확인한다. (value === true)
  • assert.isFalse(value) – value가 false인지 확인한다. (value === false)

정리

BDD에서는 스펙을 먼저 작성하고 난 후에 구현을 시작한다. 구현이 종료된 시점에서는 스펙과 코드 둘 다를 확보할 수 있다.

스펙의 용도는 다음 세가지 이다.

  1. 테스트 - 함수가 의도하는 동작을 제대로 수행하고 있는지 보장
  2. 문서 - 함수가 어떤 동작을 수행하고 있는지 설명, describeit에 설명 포함
  3. 예시 - 실제 동작하는 예시를 이용해 함수를 어떻게 사용할 수 있는지 명시

출처 : https://ko.javascript.info/testing-mocha#ref-205

0개의 댓글