함수를 하나 만들고 있다고 해보자. 대부분 매개변수 - 결과 관계를 중심으로 어떻게 코드를 작성할지 구상할 것이다.
개발 중엔 콘솔 창 등을 이용해 실제 실행 결과가 기대했던 결과와 같은지 계속 비교하면서 원하는 기능이 잘 구현되고 있는지 확인할 거다.
실제 실행 결과가 기대했던 결과와 다를 땐, 코드를 수정하고 다시 실행해 그 결과를 기대했던 결과와 다시 비교해볼 거다. 원하는 기능을 완성할 때까지 이 과정을 반복할 것 같다.
이렇게 수동으로 코드를 재실행하는 건 상당히 불완전하다.
EX.
현재 함수 f를 구현하고 있다고 가정하자.
여기서 끝? ❌ f(1)이 제대로 동작하는지 다시 확인해야함!
이렇게 테스트를 수동으로 하면 에러가 발생할 여지를 남긴다.
테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해볼 수 있는데, 이때 실행 결과와 기대 결과를 비교할 수 있다.
💡 BDD는 테스트(test), 문서(documentation), 예시(example)를 한데 모아놓은 개념이다.
실제 개발 사례를 이용해 BDD가 뭔지 차근차근 알아가보자.
x를 n번 곱해주는 함수, pow(x, n)을 구현하고 있다고 가정하자.(단, n은 자연수, 조건 n≥0을 만족해야 한다.)
사실 자바스크립트엔 거듭제곱 연산자 **가 있다. 그럼에도 불구하고 함수를 직접 구현하는 이유는, 구현 과정에 초점을 두면서 BDD를 직접 적용해 보기 위해서이다. 기능이 간단한 함수를 구현하면서 BDD를 직접 적용해보면 큰 문제에 BDD에 적용하는 건 쉬울 것이다.
본격적으로 코드를 작성하기 전, 먼저 해야할 것이 있다. 코드가 무슨 일을 하는지 상상한 후 이를 자연어로 표현해야 한다.
이때, 만들어진 산출물을 BDD에선 명세서(specification) 또는 짧게 줄여 스펙(spec)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
스펙은 세 가지 주요 구성 요소로 이루어진다.
describe("title", function() {...})
it("유스 케이스 설명"), function() {...})
assert.equal(value1, value2)
함수 assert.*는 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에선 assert.equal이 사용되었는데, 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 예시에선 pow(2,3)의 결괏값과 8을 비교한다. 비교나 확인에 쓰이는 다른 함수들은 아래에서 다시 얘기하겠다.
명세서는 실행 가능하다. 명세서를 실행하면 it 블록 안의 테스트가 실행된다.
실제 개발에 착수하면 아래와 같은 순서로 개발이 진행된다.
위와 같은 방법은 반복적인(iterative) 성격을 지닌다. 명세서를 작성하고 실행한 후 테스트를 모두 통과할 때까지 코드를 작성하고, 또 다른 테스트를 추가해 앞의 과정을 반복하기 때문이다. 이렇게 하다 보면 종래에는 완전히 동작하는 코드와 테스트 둘 다를 확보하게 된다.
이제 실제 사례에 위 개발 프로세스를 적용해보자.
함수 pow의 스펙 초안은 이미 위에서 작성했으므로, 첫 번째 단계는 이미 완료한 상태이다. 코드를 본격적으로 작성하기 전에, JS 라이브러리 몇 가지를 사용해 테스트를 실행해보자.
아마 지금 상태에선 테스트 모두가 실패하겠지만 그런데도 실행해보는 이유는, 테스트가 실제로 돌아가는지 확인하기 위해서이다.
총 3개의 라이브러리를 사용해 테스트를 진행해보겠다.
세 라이브러리 모두, 브라우저나 서버 사이드 환경을 가리지 않고 사용 가능하다. 여기서는 브라우저 환경을 가정하고 사용해보자.
<!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)의 코드가 들어감<div id="mocha">
– Mocha 실행 결과가 출력됨 지금은 함수 pow 본문에 아무런 코드도 없기 때문에 테스트가 실패할 수 밖에 없다. 지금 상황에선 pow(2,3)가 8이 아닌 undefined를 반환하기 때문에 에러가 발생한다.
참고로, karma같은 고수준의 테스트 러너를 사용하면 다양한 종류의 테스트를 자동으로 실행할 수 있다.
오로지 테스트 통과만을 목적으로 코드를 간단하게 작성해보겠다. 이제 스펙을 실행해도 에러가 발생하지 않는다.
지금까진 꼼수를 써서 코드를 작성했기 때문에, pow(3,4)를 실행하면 틀린 결과를 출력할 것이다. 하지만 테스트는 모두 통과하는 상태이다.
이렇게 테스트는 모두 통과하지만, 함수가 제 역할을 못하는 경우는 실무에서 빈번하다. 스펙이 불완전해서 그런 것이니 더 많은 유스케이스를 추가해보자.
pow(3, 4) = 81을 만족하는지 확인하는 테스트를 추가해보겠다.
스펙에 테스트를 추가하는 방법은 아래와 같이 두 가지가 있다.
assert에서 에러가 발생하면 it 블록은 즉시 종료된다. 따라서 기존 it 블록에 assert를 하나 더 추가하면 첫 번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적인 차이는 여기에 있다.
두 번째 방법처럼 it 블록을 하나 더 추가해 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있기 때문에 두 번째 방법을 추천한다.
여기에 더하여 테스트를 추가할 땐 다음 규칙도 따르는 게 좋다.
💡 테스트 하나에선 한 가지만 확인하기
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면, 이 둘을 분리하는 것이 좋다.
이제 위의 두 번째 방법을 사용해 테스트를 직접 추가해보자.사실 실패하는 게 당연하다. 함수는 항상 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("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문이 중첩 describe 안에 함께 묶여있다는 걸 눈여겨보자. makeTest는 오직 for문에서만 사용되고, 다른 데선 사용되지 않기 때문에 이렇게 묶어놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다. 위와 같이 중첩 describe를 쓰면 그룹을 만들 수 있다.
중첩 describe는 새로운 테스트 '하위 그룹(subgroup)'을 정의할 때 사용된다. 이렇게 새로 정의된 테스트 하위 그룹은 테스트 결과 보고서에 들여쓰기 된 상태로 출력된다.
만약에 미래에 자체 헬퍼 함수를 가진 it과 describe를 최상위 레벨에 추가한다면, 이들 헬퍼 함수에선 makeTest에 접근할 수 없을 것이다.
💡 before/after와 beforeEach/afterEach
함수 before는 (전체) 테스트가 실행되기 전에 실행되고, 함수 after는 (전체)테스트가 실행된 후에 실행된다. 함수 beforeEach는 매 it이 실행되기 전에 실행되고, 함수 afterEach는 매 it이 실행된 후에 실행된다.
EX.
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/afterEach와 before/after는 대개 초기화 용도로 사용된다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있다.
앞서 정의했듯이 함수 pow(x, n)의 매개변수 n은 양의 정수이어야 한다.
자바스크립트에선 수학 관련 연산을 수행하다 에러가 발생하면 NaN을 반환한다. 함수 pow도 n이 조건에 맞지 않으면 NaN을 반환해야 한다.
n이 조건에 맞지 않을 때 함수가 NaN을 반환하는지 아닌지를 검사해주는 테스트를 추가해보자.
describe("pow", function() {
// ...
it("n이 음수일 때 결과는 NaN입니다.", function() {
assert.isNaN(pow(2, -1));
});
it("n이 정수가 아닐 때 결과는 NaN입니다.", function() {
assert.isNaN(pow(2, 1.5));
});
});
스펙을 실행하면 다음과 같은 결과가 출력된다.기존엔 n이 음수거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에, 새롭게 추가된 테스트는 실패할 수밖에 없다.
실패할 수밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것이다.
💡 다양한 assertion
위에서 사용한 assert.isNaN은 NaN인지 아닌지를 확인해준다.
Chai는 이 외에도 다양한 assertion을 지원한다.
- assert.equal(value1, value2) – value1과 value2의 동등성을 확인한다(value1 == value2).
- assert.strictEqual(value1, value2) – value1과 value2의 일치성을 확인한다(value1 === value2).
- assert.notEqual, assert.notStrictEqual – 비 동등성, 비 일치성을 확인한다.
- assert.isTrue(value) – value가 true인지 확인한다(value === true).
- assert.isFalse(value) – value가 false인지 확인한다(value === false).
- 이 외의 다양한 assertion은 docs에서 확인할 수 있다.
새롭게 추가한 테스트를 통과할 수 있도록 pow를 수정해보겠다.
function pow(x, n) {
// 새로 추가한 코드 - start
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
// 새로 추가한 코드 - end
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
에러 없이 테스트를 모두 통과한다.
📌 답: 만약 첫 번째 assert에서 오류가 난다면 두번째, 세번째 assert는 실행도 되지 않고 종료된다. 이렇게 테스트 코드를 작성하면 당장은 쉽게 테스트를 진행할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기가 힘들어진다.
하나의 it 블록에서는 한 가지만 하도록, 여러 it 블록으로 쪼개어 describe 안에 넣어 수정해야한다.
이렇게 하면 에러가 발생했을 때 입력값이 무엇인지 쉽게 파악할 수 있다.
--
이 글은 https://ko.javascript.info/ 를 참고하여 작성하였습니다.