모던 JavaScript 튜토리얼을 따라가면서 정리합니다.
3.5. 테스트 자동화와 Mocha
테스트를 하는 이유
- 코드를 수동으로 재실행하면서 테스트를 하면 무언가를 놓치기 쉽다.
- 테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해볼 수 있는데, 이때 실행 결과와 기대 결과를 비교할 수 있다.
- 잘 테스트된 코드는 더 나은 아키텍처를 만든다. 테스트를 작성하려면 함수가 어떤 동작을 하는지, 입력값은 무엇이고 출력값은 무엇인지 정의하고 난 후에 구현을 시작한다. 코드는 정의된 사항을 뒷받침할 수 있게 작성해야 한다. 구현을 시작하는 순간부터 이미 좋은 아키텍처가 보장된다.
Behavior Driven Development
- BBD는 테스트(test), 문서(documentation), 예시(example)을 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
코드를 작성하기 전에 코드가 무슨 일을 하는 지 상상한 후 이를 자연어로 표현해야 한다. 이때 만들어진 산출물을 BDD에서는 명세서(specification) 또는 스펙(spec)이라고 부른다. 명세서엔 유스 케이스에 대한 자세한 설명과 테스트가 담겨 있다.
1
2
3
4
5
describe("pow", function() {
it("주어진 숫자의 n 제곱", function() {
assert.equal(pow(2, 3), 8);
});
});
스펙은 세 가지 주요 구성요소로 이루어진다.
describe("title", function() {...})
: 구현하고자 하는 기능에 대한 설명이 들어간다.it
블록을 한 데 모아주는 역할도 한다.it("유스 케이스 설명", function() {...})
: 첫 번째 인수에는 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어준다. 두 번째 인수에는 유스 케이스 테스트 함수가 들어간다.assert.equal(value1, value2)
: 기능을 제대로 구현했다면it
블록 내의 코드assert.equal(value1, value2)
이 에러 없이 실행된다. 함수assert.*
는pow
가 예상한 대로 동작하는지 확인해준다.
명세서는 실행 가능하다. 명세서를 실행하면 it
블록 안의 코드가 실행된다.
개발 순서
- 명세서 초안을 작성한다. 초안엔 기본적인 테스트도 들어간다.
- 명세서 초안을 보고 코드를 작성한다.
- 코드가 작동하는지 확인하기 위해 Mocha라 불리는 테스트 프레임워크를 사용해 명세서를 실행한다. 이때, 코드가 잘못 작성되었다면 에러가 출력된다. 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드를 수정한다.
- 모든 테스트를 통과하는 코드 초안이 완성되었다.
- 명세서에 지금까진 고려하지 않았던 유스케이스 몇 가지를 추가한다. 테스트가 실패하기 시작할 것이다.
- 세 번째 단계로 돌아가 테스트를 모두 통과할 때까지 코드를 수정한다.
- 기능이 완성될 때까지 3~6단계를 반복한다.
위와 같은 방법은 반복적인(iterative) 성격을 지닌다.
스펙 실행하기
- Mocha – 핵심 테스트 프레임워크로,
describe
,it
과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공한다. - Chai – 다양한 assertion을 제공해 주는 라이브러리이다.
- Sinon – 함수의 정보를 캐내는 데 사용되는 라이브러리로, 내장 함수 등을 모방한다.
세 라이브러리 모두 브라우저나 서버사이드 환경을 가리지 않고 사용 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!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()
– 테스트를 실행시켜주는 명령어
코드 초안
1
2
3
function pow(x, n) {
return 8;
}
스펙 개선하기
스펙을 개선할 때 기존 it
블록에 assert
하나 더 추가하는 방법과, 테스트 하나 더 추가하는 방법 (it
블록 하나 더 추가하기)이 있다. assert
에서 에러가 발생하면 it
블록은 즉시 종료된다. 따라서 기존 it
블록에 assert
를 하나 더 추가하면 첫 번째 assert
가 실패했을 때 두 번째 assert
의 결과를 알 수 없다. 따라서 두 번째 방법처럼 it
블록을 하나 더 추가해 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있기 때문에 두 번째 방법이 권장된다.
또한 테스트 하나에선 한 가지만 확인하는 것이 좋다. 연관이 없는 사항 두 개를 테스트 하나에서 점검한다면 이 둘을 분리하자.
코드 개선하기
1
2
3
4
5
6
7
8
9
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
함수가 제대로 동작하는지 확인하기 위해 더 많은 값을 테스트해보자. 수동으로 여러 개의 it
블록을 만드는 대신 for
문을 사용해 자동으로 it
블록을 만들 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
는 새로운 테스트 ‘하위그룹(subgroup)’을 정의할 때 사용된다. 이렇게 새로 정의된 테스트 하위 그룹은 테스트 결과 보고서에 들여쓰기 된 상태로 출력된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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을 사용해 이 아래에 더 많은 테스트를 추가할 수 있습니다.
});
before/after
와 beforeEach/afterEach
함수 before
은 전체 테스트가 실행되기 전에 실행되고, 함수 after
는 전체 테스트가 실행된 후에 실행된다. 함수 beforeEach
는 매 it
이 실행되기 전에 실행되고, 함수 afterEach
는 매 it
이 실행된 후에 실행된다.
스펙 확장하기
자바스크립트에선 수학 관련 연산을 수행하다 에러가 발생하면 NaN
을 반환한다. 함수 pow
도 n
이 조건에 맞지 않으면 NaN
을 반환해야 한다. 이를 검사해주는 테스트를 추가해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
describe("pow", function() {
// ...
it("n이 음수일 때 결과는 NaN입니다.", function() {
assert.isNaN(pow(2, -1));
});
it("n이 정수가 아닐 때 결과는 NaN입니다.", function() {
assert.isNaN(pow(2, 1.5));
});
});
기존엔 n
이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에, 새롭게 추가한 테스트는 실패할 수밖에 없다.
BDD의 핵심은 여기에 있다. 실패할 수밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것이다.
테스트를 통과할 수 있게 다음과 같이 코드를 개선한다.
1
2
3
4
5
6
7
8
9
10
11
12
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;
}