테스트 자동화는 현업에서도 빈번히 사용된다. 테스트 자동화는 왜 해야하는 것이며 어떻게 하는 것인지에 대해서 알아보자.
함수를 하나 만들고 있다고 가정했을 때, 보통 매개변수 - 결과 관계를 중심으로 어떻게 코드를 작성할 지 구상한다. 개발 중에는 콘솔 창등을 이용해 원하는 기능이 잘 작동하는지 계속해서 확인하곤 한다. 원하는 기능이 작동할때 까지 수정 → 확인 → 수정 → 확인 이 과정을 반복하는데, 이렇게 수동으로 코드를 재실행 하는 것은 상당히 불완전하다.
코드를 수동으로 재실행 하면서 테스트를 하면 무언가를 놓치기 쉽기 때문이다.
예를 들어, 함수 f를 구현할 때 코드를 작성하고 f(1)이 제대로 동작하는지 확인한다. f(1)은 제대로 동작하지만 f(2)는 제대로 동작하지 않는다. 그렇다면 f(2)를 제대로 동작하도록 코드를 수정한다. 수정 후에 f(2)가 제대로 동작한다. 하지만 여기서 끝이 아니다. 다시 한 번 f(1)도 제대로 동작하는지 다시 확인해주어야 한다. 이렇게 테스트를 수동으로 하면 에러가 발생할 여지를 남기게 된다.
따라서, 테스트를 하는 이유는 다음과 같다.
💡 개발자는 어떤 기능을 구현할 때 머릿속에 수많은 유스 케이스를 생각하며 코드를 작성하는데, 코드 변경시 모든 유스케이스를 상기하면서 코드를 수정하는 것은 불가능하며 하나를 고치면 또 다른 문제가 튀어나오는 오류가 발생한다. 그렇기 때문에 테스트를 자동화 하는 것이다.
❗ 테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데, 이때 실행 결과와 기대 결과를 비교할 수 있다.
테스트 자동화 방법론에는 Behavior Driven Development (BDD)라 불리는 방법론이 있다.
BDD는 테스트(Test), 문서(Documentation), 예시(Example)를 한데 모아놓은 개념이다.
실제 개발 사례를 이용해 BDD가 무엇인지 알아보자.
x
를 n
번 곱해주는 함수 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
블록 안의 테스트가 실행된다.
실제로 개발을 하게되면 아래와 같은 순서로 개발이 진행된다.
Mocha
라 불리는 테스트 프레임워크를 사용해 명세서를 실행한다. 이때, 코드가 잘못 작성되었다면 에러가 출력된다. 개발자는 테스트를 통과해 에러가 더는 출력되지 않을 때까지 코드를 수정한다.위와 같은 방법은 반복적인(iterative) 성격을 지닌다. 이 과정을 계속 하다보면 끝으로는 완전히 동작하는 코드와 테스트 둘 다를 확보하게 된다.
이제 실제 사례에 위 개발 순서를 적용해보자.
함수 pow
의 스펙 초안은 이미 위에서 작성했으므로, 첫 단계는 끝난 상태이다.
해당 튜토리얼에선 총 3개의 라이브러리를 사용해 테스트를 진행한다. 각 라이브러리에 대한 설명은 아래와 같다.
세 라이브러리 모두 브라우저나 서버 사이드 환경을 가리지 않고 사용 가능하다. 여기에서는 브라우저 환경을 가정하고 사용해본다.
아래 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>
위 페이지는 다섯 부분으로 나눌 수 있다.
<head>
- 테스트에 필요한 서드파티 라이브러리와 스타일을 불러온다.<script>
- 테스트할 함수 pow
의 코드가 들어간다.describe("pow", ...)
를 외부 스크립트(test.js
)에서 불러온다.<div id="mocha">
- Mocha 실행 결과가 출력된다.mocha.run()
- 테스트를 실행시켜주는 명령어이다.이대로 스펙을 실행시키면 다음과 같은 에러가 발생한다.
지금은 함수 pow
본문에 아무런 코드가 없기 때문에 pow(2,3)
이 undefined
를 반환하여 테스트가 실패할 수 밖에 없다.
오로지 테스트 통과만을 목적으로 코드를 간단하게 작성해보자.
function pow(x,n) {
return 8; // 속임수 사용
}
이제 스펙을 실행해도 에러가 발생하지 않는다.
위에서는 속임수를 사용해 코드를 작성하여 테스트를 모두 통과한다.
하지만 함수가 제 역할을 못하기 때문에 더 많은 유스케이스를 추가해보자.
pow(3,4) = 81
을 만족하는지 확인하는 테스트를 추가해보자.
스펙에 테스트를 추가하는 방법은 두가지가 있다.
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
블록을 하나 더 추가해 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있기 때문에 두 번째 방법을 추천한다.
여기에 더하여 테스트를 추가할 땐 다음 규칙도 따르는 것이 좋다.
테스트 하나에선 한 가지만 확인하기
테스트 하나에서 연관이 없는 사항 두개를 점검하고 있다면 분리하는 것이 좋다.
아래는 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("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
문에서만 사용되고, 다른 데서는 사용되지 않기 때문에 위와 같이 묶어두었다. 위 스펙에서 makeTest
와 for
문은 함께 어우러져 pow
가 제대로 동작하는지 확인해주는 역할을 한다.
이렇게 중첩 describe
를 사용하면 그룹을 만들 수 있다.
중첩 describe
는 새로운 테스트 '하위 그룹(subgroup)'을 정의할 때 사용된다. 이렇게 새로 정의된 테스트 하위 그룹은 테스트 결과 보고서에 들여쓰기 된 상태로 출력된다.
만약 나중에 자체 헬퍼 함수를 가진 it
과 describe
를 최상위 레벨에 추가한다면 이들 헬퍼 함수에선 makeTest
에 접근할 수 없다.
함수 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/afterEach
와 before/after
는 대개 초기화 용도로 사용된다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때 (또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업들이 있으면 이들을 이용 할 수 있다.
첫 번째 반복에선 함수 pow
의 기본적인 기능을 구현해보았다. 또 다른 반복을 돌며 기능을 개선해보자.
앞서 정의했듯이 함수 pow(x,n)
의 매개변수 n
은 양의 정수여야 한다.
자바스크립트에서는 수학 관련 연산을 수행하다 에러가 발생하면 NaN
을 반환한다.
함수 pow
도 n
이 조건에 맞지 않을 때 함수가 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;
}
이제 에러 없이 테스트를 모두 통과하는 것을 확인할 수 있다.
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
)BDD에서는 스펙을 먼저 작성하고 난 후에 구현을 시작한다. 구현이 종료된 시점에서는 스펙과 코드 둘 다를 확보할 수 있다.
스펙의 용도는 다음 세가지 이다.
describe
와 it
에 설명 포함