JavaScript 3.5
테스트 자동화와 Mocha
테스트는 왜 해야 하는가?
- 코드를 수동으로 재실행 하면서 테스트를 하면 무언가를 놓치기 쉽다.
- 테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데, 이때 실행 결과와 기대 결과를 비교할 수 있다.
Behavior Driven Development
- BDD
- 테스트, 문서, 예시를 한데 모아놓은 개념.
명세서 specification, spec
- 스펙은 세 가지 주요 구성 요소로 이루어진다.
- Describe(“title”, function() {…})
- 구현하고자 하는 기능에 대한 설명, it 블록을 모아주기도 한다.
- it(“유스케이스 설명”, function() {…})
- 유스케이스에 대한 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적는다.
- 유스케이스 테스트 함수가 두번째 인수로 들어간다.
- assert.*(value1, value2)
- 기능을 제대로 구현해서 예상대로 동작되는지 확인한다.
- Describe(“title”, function() {…})
- 거듭제곱 함수와 명세서
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;
}
요약
- 스펙의 용도는 세가지이다.
- 테스트 : 함수가 의도하는 동작을 제대로 수행하고 있는지 보장.
- 문서 : 함수가 어떤 동작을 수행하고 있는지 설명.
- 예시 : 실제 동작하는 예시를 이용해 함수를 어떻게 사용할 수 있는지 알려줌.
- 스펙이 있기 때문에 개발자는 안전하게 함수를 개선하거나 변경할 수 있다.
- 코드가 바뀌어도 기존에 구현된 기능에 영향을 주지 않게 하는 것이 중요한데 테스트 자동화는 이것이 가능하게 만들어준다.
- 잘 테스트 된 코드는 더 나은 아키텍쳐를 만든다.