테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
<!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>
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
테스트의 필요성
어떤 코드를 작성하고 그 코드가 원하는 결과와 같은 결과인지를 알아보기 위해서 코드를 실행하고 실행 결과가 기대했던 결과와 다를 경우에 코드를 수정 하고, 또 실행하고를 반복한다. 하지만 이렇게 코드를 ‘재실행’ 하는 것은 상당히 불완전하다
코드를 수동으로 ‘재실행’ 하면서 테스트를 하면 무엇인가 놓치기 쉽다.
예를 하나 들어보자
- f(1)이 제대로 동작하는가 ⇒ yes
- f(2)가 제대로 동작하는가 ⇒ No
- f(2) 수정
- f(2)가 제대로 동작하는가 ⇒ Yes
f(2)가 제대로 동작하면 끝일까? 그건 아니다. 왜냐하면 f(1)이 제대로 동작하는지 확인을 못했기 때문이다. 개발자는 무엇인가 만들때 머릿속에 수많은 유즈케이스를 생각하며 코드를 작성해야 하는데 코드를 변경할 때마다 모든 유즈케이스를 상기하면서 코드를 수정하는 것은 거의 불가능하다.
테스팅 자동화는 테스트 코드가 실제 동작에 관여하는 코드와 별개로 작성되었을 때 가능하다. 테스트 코드를 이용하면 함수를 다양한 조건에서 실행해 볼 수 있는데 이때 실행 결과와 다른 기대 결과를 비교할 수 있다.
Behavior Driven Development
BDD
테스트(test), 문서(document), 예시(example)를 한데 모아놓은 개념이다.
거듭제곱 함수와 명세서
자바스크립트에는 거듭제곱 연산자가 이미 있지만 구현 과정에 초점을 두면서 BDD를 적용해보자. 항상 코드는 작성 전에 코드가 무슨일을 하는지 상상한 후 이를 자연어로 표현할 것
이때, 만들어진 산출물을 BDD에서는 산출물(specification) 또는 짧게 줄여 스펙(sepc)이라고 부른다. 명세서에는 아래와 같이 유스케이스에 대한 자세한 설명과 테스트가 담겨있다.
describe("pow", function() {
it("주어진 숫자의 n 제곱", function(){
assert.equal(pow(2, 3), 8);
})
})
스펙의 구성 요소
**describe(”title”, function(){ … })**
구현하고자 하는 기능에 대한 설명이 들어간다. 우리 예시에선 함수 pow가 어떤 동작을 하는지에 대한 설명이 들어간다. it 블록을 한데 모아주는 역할도 한다.
**it(”유스 케이스 설명”, function() { … })**
it의 첫 번째 인수엔 특정 유스 케이스에 대한 설명이 들어간다. 이 설명은 누구나 읽을 수 있고 이해할 수 있는 자연어로 적어둔다. 두번째 인수엔 유스케이스 테스트 함수가 들어간다.
assert.equal(value1, value2)
함수 assert.*은 pow가 예상한 대로 동작하는지 확인해준다. 위 예시에서는 assert.equal이 사용되었는데 이 함수는 인수끼리 동등 비교했을 때 다르다고 판단되면 에러를 반환한다. 위 예시에는 pow(2,3)의 결과값고 8을 비교할 것이다.
실제 개발 순서
- 명세서 초안 작성. 초안에는 기본적인 테스트도 포함됨
- 명세서 초안을 보고 코드 작성
- 코드가 작동하는지 확인하기 위해서. Mocha라고 불리는 테스트 프레임워크 사용해 명세서 실행. 이때 코드가 잘못되었다면 에러 출력 개발자는 테스트를 모두 통과해 에러가 더는 출력되지 않을 때까지 코드 수정
- 모든 테스트를 통과하는 코드 초안 완성
- 명세서에 새로운 유스케이스 몇 가지 추가. 테스트 실패하기 시작함
- 세 번째 단계로 돌아가서 테스트를 모두 통과할 때까지 코드 수정
- 기능이 완성될 때 까지 3~6 단계 반복
스펙 실행하기
총 3개의 라이브러리를 사용해서 테스트 진행함
- Mocha → 핵심 테스트 프레임워크로, describe, it과 같은 테스팅 함수와 테스트 실행 관련 주요 함수를 제공함
- Chai → 다양한 assertion을 제공해주는 라이브러리 위 예시에서 assert.equal을 사용했다
- Sinon → 함수의 정보를 캐내는 데 사용되는 라이브러리, 내장함수를 모방한다.
mocha.setup('bdd'); // 기본 셋업
// chai의 다양한 기능 중, assert를 전역에 선언합니다.
let assert = chai.assert;
function pow(x, n) {
/* 코드를 여기에 작성합니다. 지금은 빈칸으로 남겨두었습니다. */
}
mocha.run(); // 테스트를 실행시켜주는 명령어
코드 초안
이 코드는 오직 테스트 통과만을 위해서 작성한 코드이다.
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("주어진 숫자의 n제곱", function(){
assert.equal(pow(2, 3), 8);
}
it("3을 네 번 곱하면 81입니다.", function(){
assert.equal(pow(3, 4), 81);
})
})
assert에서 에러가 발생하면 it블록은 즉시 종료된다. 따라서 기존 it블록에 assert를 하나 더 추가하면 첫번째 assert가 실패했을 때 두 번째 assert의 결과를 알 수 없다. 두 방법의 근본적 차이는 테스트를 분리해서 작성하면 더 많은 정보를 얻을 수 있다.
테스트를 추가할땐 테스트 하나에선 한가지만 확인하기 규칙을 따르자
테스트 하나에서 연관이 없는 사항 두 개를 점검하고 있다면 이 둘을 분리하는게 좋다.
⇒ 테스트 실행
코드 개선
두번째 테스트도 통과할 수 있게 코드를 개선해보자
function pow(x, n){
let result = 1;
for(int 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
아래 예시에서는 헬퍼함수 makeTest와 for문이 중첩 describe안에 함께 묶여 있다. makeTest는 오직 for문에서만 사용되고 다른데서는 사용하지 않기 때문에 이렇게 묶어 놓았다. 아래 스펙에서 makeTest와 for문은 함께 어우러져 pow가 제대로 동작하는지 확인해주는 역할을 한다.
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)
beforeEach/afterEach와 before/after는 대개 초기화 용도로 사용됩니다. 카운터 변수를 0으로 만들거나 테스트가 바뀔 때(또는 테스트 그룹이 바뀔 때)마다 해줘야 하는 작업이 있으면 이들을 이용할 수 있습니다.
스펙 확장하기
자바스크립트에서 수학 관련 연산을 수행하다 에러가 발생하면 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이 음수이거나 정수가 아닌 경우를 생각하지 않고 구현했기 때문에 새롭게 추가한 테스트는 실패할 수 밖에 없다. BDD의 핵심은 여기 있다. 실패할 수 밖에 없는 테스트를 추가하고, 테스트를 통과할 수 있게(에러가 발생하지 않게) 코드를 개선하는 것
다양한 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){
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;
}
문제
잘못된 점 찾기
함수 pow의 테스트가 뭔가 잘못되었다.
it("주어진 숫자의 n 제곱", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
테스트는 문제 없이 통과하지만 문제가 있다. 위 코드엔 세개의 assert가 있지만 결론적으로 테스트 함수는 하나뿐이다. 이렇게 코드를 작성하면 테스트 자체를 쉽게 통과할 수 있지만, 에러가 발생했을 때 에러의 원인을 찾기 힘들어진다.
실행 흐름이 복잡할 경우엔 에러를 만든 입력값이 무엇인지 일일이 체크해야하기 때문에 테스트코드를 디버깅해야 할 수도 있다.
테스트는 명확한 입력값, 출력값과 함께 여러 개의 it 블록으로 쪼개 작성하는 것이 좋다.
describe("주어진 숫자의 n 제곱", function() {
it("5를 1 제곱하면 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5를 2 제곱하면 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5를 3 제곱하면 125", function() {
assert.equal(pow(5, 3), 125);
});
});
이렇게 하면 어떤 입력값이 들어왔을 때 에러가 발생하는지 정확하게 알 수 있다. 또한 이런 방식을 사용하면 it.only를 사용해 원하는 테스트만 실행할 수도 있다.
'JavaScript 개념' 카테고리의 다른 글
iterable 객체 (0) | 2024.04.30 |
---|---|
문자열 (1) | 2024.04.30 |
숫자형 (0) | 2024.04.29 |
원시값의 메서드 (0) | 2024.04.29 |
객체 (0) | 2024.04.29 |