TDD, BDD. 테스트 주도 개발 방법론에 대한 관심이 많이 있었는데 실제 써볼 기회는 많지 않았다. 이번 기회에 TDD에 대한 개념을 정리해 보고 javascript에서 사용할 수 있는 Test Framework에 대해 한번 공부해 보고자 한다. 찾아보니 Jasmine, Karma, JUnit, 또 최근 페이스북에서 만든 오픈소스 Jest 등 정말 종류가 다양하다. 그중 평소에 눈여겨 보았던 Mocha에 대해 다뤄보려 한다.
TDD?
만약 새로운 기능이 추가된다거나 유지 보수를 위해서 코드를 수정하게 될 경우 위 Flow를 다시 따르게 되는데, 여기서 코드 규모가 커질 수록 비용이 기하급수적으로 커지는 부분이 있다. 바로 테스트 부분이다. 실제 완성된 프로그램에서 특정 부분을 고치는건 상당히 스트레스를 받는 일이다. 고친 부분이 잘 돌아가도 다른부분에서 내가 의도치 않은 부작용이 발생할 가능성이 있기 때문이다. 물론 프로그래머는 함수의 응집력을 높히고 결합도를 낮추는 방식으로 개발을 할 것이다. 최대한 의존성을 낮춰서 개발한다 하더라도 어쩔수 없이 영향을 받는 함수는 존재하기 마련이며, 이에 따라 어떤 Side-Effect가 발생할지 모두 고려해서 수정을 해야 한다. 만약 어플리케이션 규모가 작다면 고려해야할 부분도 적고, 이에 따라 수정에 의해 테스트 하는 시간도 적을 것이다. 하지만 기능이 다양한 대규모 어플리케이션에서는 작은 수정 만으로도 어떤 부작용이 발생할지 모두 예측하기가 어렵다. 따라서 내가 만든 코드가 의도한 결과를 산출해 내는지 보장해주는 것이 있다면 코드를 수정하는데 드는 비용을 최소한으로 줄일 수 있을 것이다.(예를 들면 테스트하는데 드는 시간, Side-Effect를 고려해 수정해야 하는 스트레스 등)
여기게 착안한 개발 방식이 바로 테스트 주도 개발 방법론인 TDD이다. TDD Flow는 아래와 같다.
함수 설계 -> 테스트 코드 작성-> 코드 작성
기존 개발 방식과 다른점은 테스트 코드를 먼저 작성한다는 것이다. 함수 설계 후 의도한 결과를 바탕으로 테스트 코드를 작성한다. 이 후 테스트 코드가 의도한 결과가 나올 때 까지 수정 한 후, 완성된 테스트 코드를 바탕으로 실제 코드를 작성한다. 이렇게 개발하면 중간 산출물로 테스트 코드가 남게 되는데, 이후 리팩토링(Refactoring)등 소스코드 수정 후 수동으로 테스트 해볼 필요 없이 이전에 만든 테스트 코드를 돌려서 통과하는지만 체크하면 된다. 따라서 프로그래머는 부작용이 발생하더라도 적은 비용(그냥 테스트 코드만 쭉 돌려보면 되니까)으로 이를 탐지할 수 있기 때문에 테스트 비용을 크게 줄일 수 있다.
말로 설명하면 잘 이해가 안될 수 있으니 실제로 만들어 보도록 하자. Mocha에 대한 간단한 사용법을 익힌 후 TDD 방법으로 한번 만들어 보도록 하자.
Install
$ sudo npm install mocha -g
Basic
const assert = require('assert'); // node.js에서 제공하는 assert module
describe('Array', function() { // Test 상황에 대해 묘사한다.
describe('#indexOf()', function() {
// 실제 테스트 하는 함수를 콜백으로 작성한다.
it('equal success', function() {
assert.equal(-1, [1,2,3].indexOf(4)); // 두 입력값이 같은지 체크
});
it('equal fail', function() {
assert.equal(1, [1,2,3].indexOf(4)); // 두 입력값이 같은지 체크
});
});
});
실행하는 방법은 간단하다. 터미널을 열고 mocha라고 쓴 후 실행하면 test폴더 내의 모든 describe를 실행한다. 만약 특정 파일만 실행히키고 싶으면 mocha <file_name> 으로 실행하면 된다.
$ mocha
위 코드를 실행시켜보면 terminal에 다음과 같은 결과를 얻을 수 있다. describe부분은 텍스트로 출력되고, it으로 작성한 부분은 체크표시로 성공 여부를 출력한다.
[그림] Basic Test Result
Asynchronous
describe('async', function() {
// 비동기 함수를 테스트한다.
// 기본 타임아웃 시간은 2000ms 이다.
it('async without timeout', function(done) {
setTimeout(function (err) {
if (err) done(err);
else done();
}, 1000)
});
it('async with timeout', function(done) {
setTimeout(function (err) {
if (err) done(err);
else done();
}, 3000)
});
});
만약 타임아웃 시간을 바꾸고 싶은 경우엔 this.timeout(ms); 를 사용하면 된다.
test/timeout.js
describe('timeout', function() {
this.timeout(500);
it('should take less than 500ms', function(done){
setTimeout(done, 300);
});
it('should take more than 500ms', function(done){
setTimeout(done, 600);
});
});
Hooks
const assert = require('assert');
describe('hooks', function() {
before(function() {
// 모든 테스트 실행 전 한번만 실행
});
after(function() {
// 모든 테스트 실행 후 한번만 실행
});
beforeEach(function() {
// 각각의 테스트 실행 전 실행
});
afterEach(function() {
// 각각의 테스트 실행 후 실행
});
// test cases
it('test case 1', function () {
});
it('test case 2', function () {
});
});
TDD Example
- 두 수를 입력 받아 결과 값을 리턴한다.
- x, y는 number만 받을 수 있다.
- 허용되지 않은 값이 들어올 경우 invalid String을 return 한다.
const assert = require('assert');
describe('example', function() {
// sum (x, y)
// 두 수를 입력 받아 결과값을 리턴하는 함수
// x, y는 number만 받을 수 있다.
// 허용되지 않은 값이 들어올 경우 invalid String을 return 한다.
it('sum func success', function () {
assert.equal(2 , sum(1, 1)); // basic
assert.equal(15, sum(5, 10)); // basic 2
assert.equal(-5, sum(5, -10)); // -가 들어올 경우
});
it('sum func invalid', function () {
assert.equal(2 , sum(1, "1")); // string이 들어올 경우
assert.equal('invalid' , sum(1, true)); // 허용되지 않은 값이 들어올 경우
})
});
자 이제 테스트 코드를 작성했으니 실제 사용되는 sum(x, y) 함수를 작성해 보자. src폴더에 sum.js 파일을 만든 후 앞에서 정의한 조건에 맞춰 비지니스 로직을 구현하자. 먼저 생각나는 대로 x, y를 더해서 return하는 비지니스 로직을 구현하자.
src/sum.js
module.exports = function (x, y) {
return x+y;
};
이제 example.js에서 sum.js파일을 가져와서 테스트 코드에서 사용해 보도록 하자.
test/example.js
const assert = require('assert');
describe('example', function() {
const sum = require('../src/sum');
it('sum func success', function () {
assert.equal(2 , sum(1, 1)); // basic
assert.equal(15, sum(5, 10)); // basic 2
assert.equal(-5, sum(5, -10)); // -가 들어올 경우
});
it('sum func invalid', function () {
assert.equal('invalid', sum(1, "1")); // string이 들어올 경우
assert.equal('invalid', sum(1, true)); // 허용되지 않은 값이 들어올 경우
})
});
이제 mocha를 이용해서 테스트 코드를 돌려보도록 하자. 터미널에 다음 명령어를 실행한다.
$ mocha test/example.js
[그림] example.js Test Result
예상 했듯이 'sum func success' case는 모두 성공했다. 하지만 두번째 sum func invalid case는 실패했다. 이제 두번째 테스트 케이스만 통과하면 내가 의도한 ㄴsum함수가 완성된다. 이제 invalid 조건도 만족하도록 소스를 수정하도록 하자. typeof 키워드를 사용해서 x,와 y가 number일 경우에만 더하고, 아닐 경우 invalid를 리턴하도록 하면 될 것 같다.
src/sum.js
module.exports = function (x, y) {
if(typeof x === 'number' && typeof y === 'number'){
return x+y;
}
else {
return 'invalid';
}
};
다시 테스트 케이스를 돌려보도록 하자.
[그림] example.js Test Result
모든 테스트 케이스를 통과 했다. 이제 내가 원하던 함수와 그 결과를 보장하는 테스트 케이스를 모두 얻었다. 추후 이 함수의 요구사항은 그대로 리팩토링을 했을 경우, 수정 후에 테스트 케이스를 돌린 후 제대로 통과하는지만 체크해 보면 될 것이다. 지금은 간단한 하나의 테스트 케이스만 작성했지만 조건이 많아질수록, 함수가 많아질수록 나중에 수정했을 경우의 리스크와 비용은 엄청나게 줄어들 것이다.
마치며
'JavaScript' 카테고리의 다른 글
Currying in Javascript (0) | 2017.07.23 |
---|---|
Functional Programming in Javascript (8) | 2017.07.22 |
Javascript Image Filter - convolution (0) | 2017.05.02 |
Javascript ES6 Proxy (1) | 2017.04.16 |
Javascript Image Filter 만들기 (5) | 2017.04.15 |