22. 10. 14 자바스크립트) 테스트

divedeepp·2022년 10월 14일
0

JavaScript

목록 보기
3/11
post-thumbnail

테스트는 왜 해야 하는가?

개발자들은 보통 콘솔 창 등을 사용하며, 실행한 결과가 기대했던 결과와 같은지 계속 비교하면서 원하는 기능이 잘 구현되는지 확인한다.

실제 실행 결과가 기대했던 결과와 다르면, 코드를 수정하고 다시 실행하여 기대했던 결과와 비교하는 과정을 원하는 기능을 완성할 때까지 반복한다.

이렇게 코드를 수동으로 재실행하면서 테스트하는 방식은 불완전하다.

예를 들어, 함수 f를 구현한다고 가정하자.

코드를 작성하고 f(1)이 제대로 동작하는지 확인한다. 제대로 동작하는 것을 확인하고, f(2)를 테스트하지만 제대로 동작하지 않는다. 코드를 수정하고 f(2)를 재확인한다. 이번엔 제대로 동작한다. 지금 시점에 f(1)이 제대로 동작할까? 테스트하지 않았으므로 불분명하다.

이렇게 수동으로 테스트를 하면 에러가 발생할 확률이 생긴다.

이러한 문제들은 테스트 자동화를 통해 다양한 조건에서 코드를 실행하여, 실행 결과와 기대 결과를 비교할 수 있다.


Behavior Driven Development(BDD)

테스트(test), 문서(documentation), 예시(example)를 한데 모아놓은 개념이다.

명세서(specification, spec)

코드를 작성하기 전에, 코드가 무슨 일을 하는지 상상한 후 이를 자연어로 표현해야 한다.

이 때, 만들어진 산출물을 BDD에선 명세서 또는 스펙이라 부른다.

스펙엔 아래와 같이 유스 케이스에 대한 자세한 설명과 테스트가 담겨있다.

describe("pow", function() {

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

});

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

  • describe("title", function() { ... }) : 구현하고자 하는 기능에 대한 설명이 들어간다.
  • it("유스 케이스 설명", function() { ... }) : it의 첫 번째 인수에는 특정 유스 케이스에 대한 설명이 들어간다. 누구나 읽을 수 있고, 이해할 수 있는 자연어로 적어준다. 두 번째 인수에는 해당 유스 케이스에 대한 테스트 함수가 들어간다.
  • assert.equal(value1, value2) : 기능을 제대로 구현했다면, it 블록 내의 코드 assert문이 에러 없이 실행된다. 함수 assert.*는 구현하고자 하는 기능이 제대로 동작하는지 확인해 준다. 위 예시에서 사용된 assert.equal는 인수끼리 동등 비교를 했을 때, 다르다고 판단되면 에러를 반환한다.

스펙은 실행 가능하다. 스펙을 실행하면 it 블록 안의 테스트가 실행된다.

다양한 assertion

Chai는 다양한 assertion을 지원한다.

  • assert.equal(value1, value2) : value1과 value2의 동등성을 확인한다.
  • assert.strictEqual(value1, value2) : value1과 value2의 일치성을 확인한다.
  • assert.notEqual : 비동등성을 확인한다.
  • assert.notStrictEqual : 비일치성을 확인한다.
  • assert.isTrue(value) : value가 true인지 확인한다.
  • assert.isFalse(value) : value가 false인지 확인한다.
  • 이외에도 다양한 assertion을 확인할 수 있다. https://www.chaijs.com/api/assert/

BDD를 적용한 기능 개발 프로세스

실제 개발에 착수하면 아래와 같은 순서로 개발이 진행된다.

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

위 프로세스를 따라서 거듭제곱 함수를 구현하는 예시로 BDD를 알아보자.

1) 스펙 초안 작성

pow 기능을 구현해보자.

아래 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="test.js"></script>

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

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

</html>
// test.js
describe("pow", function() {

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

});

위 HTML을 웹페이지에 불러와 함수 pow 본문에 아무 코드도 없기 때문에 테스트에 실패한다. 결과는 다음과 같다.

2~4) 기능 코드 추가하고 테스트하기

테스트 통과를 위해 코드를 간단하게 추가해보자.

function pow(x, n) {
  return 8;
}

결과는 다음과 같다. 에러가 발생하지 않는다.

5) 스펙에 유스 케이스 추가하기

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

  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의 결과를 알 수 없다. 그렇기 때문에 두 번째 방법으로 테스트를 분리하는 것을 추천한다.

또, 테스트를 추가할 땐 다음 규칙도 따르는게 좋다. 테스트 하나에선 한 가지만 확인하기.

위 테스트의 결과는 다음과 같다.

6) 코드 개선하기

두 번째 테스트도 통과할 수 있게 코드를 개선해보자.

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

결과는 다음과 같다.

7) 스펙 더 확장하기

함수 pow의 매개변수 n은 양의 정수여야 한다.

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

pow도 해당 에러에 대한 테스트를 추가해보자.

describe("pow", function() {

  // ...

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

결과는 다음과 같다. 에러 없이 모든 테스트를 통과한다.

이제 원하는 기능을 완성할 때까지 위 과정들을 계속 반복하면 된다.

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

중첩 describe는 새로운 테스트 하위 그룹을 정의할 때 사용된다.

확장) 함수 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)

위 함수들은 대개 초기화 용도로 사용된다.

카운터 변수를 0으로 만들거나, 테스트가 바뀔 때마다 해줘야하는 작업이 있을 때 활용할 수 있다.

BDD를 적용함으로써 얻는 이점

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

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

  • 테스트 : 함수가 의도하는 동작을 제대로 수행하고 있는지 보장한다.
  • 문서 : 함수가 어떤 동작을 수행하고 있는지 설명해준다.
  • 예시 : 실제 동작하는 예시를 이용해 함수를 어떻게 사용할 수 있는지 알려준다.

스펙이 있기 때문에 개발자는 안전하게 함수를 개선하거나 변경할 수 있다.

코드가 바뀌어도 기존에 구현된 기능에 영향을 주지 않게 하는 건 매우 중요하다. 프로젝트 규모가 커지면 함수 하나를 이곳저곳에서 사용하는데, 변경된 함수가 이 함수를 사용하는 모든 곳에서 제대로 동작하는지 수동으로 확인하는 건 불가능하기 때문이다.

테스트를 하지 않고 코드를 작성해왔다면 개발자들은 둘 중 한 갈래의 길로 빠져버린다.

  • 아무 대책 없이 코드를 변경한다. 부작용을 생각하지 않고 함수를 수정했기 때문에 어디선가 버그가 발생한다.
  • 수정이나 개선을 기피하게 된다. 버그의 대가가 가혹하기 때문에, 코드가 구식이 되어도 그 누구도 코드를 건드리려 하지 않는다.

테스트 자동화는 이런 문제를 피하게 도와준다. 코드에 변화가 있어도 스펙을 실행하여 테스트를 진행하면 에러 발생 여부를 확인할 수 있다.

또, 잘 테스트 된 코드는 더 나은 아키텍처를 만든다. 테스트를 작성하려면 함수가 어떤 동작을 하는지, 입력값은 무엇이고 출력값은 무엇인지 정의하고 난 후에 구현을 시작한다. 코드는 정의된 사항을 뒷받침 할 수 있게 작성해야한다. 따라서 구현을 시작하는 순간부터 이미 좋은 아키텍처가 보장된다.

매 번 이런 절차를 따라 구현하는게 쉽지는 않다. 함수가 어떻게 동작해야하는지 확신이 서지 않는 상황에서 코드를 작성하기전에 명세서를 작성해야하므로 익숙하지 않을 수 있다. 그렇지만 테스트를 작성하면 일반적으로 개발 속도가 빨라지고 이전보다 코드를 더 안정적으로 작성할 수 있다.


참고 문헌

https://ko.javascript.info/code-quality

profile
더깊이

0개의 댓글