본문 바로가기

Language/JavaScript

TDD & Mocha - Javascript Test Framework

 TDD, BDD. 테스트 주도 개발 방법론에 대한 관심이 많이 있었는데 실제 써볼 기회는 많지 않았다. 이번 기회에  TDD에 대한 개념을 정리해 보고 javascript에서 사용할 수 있는 Test Framework에 대해 한번 공부해 보고자 한다.  찾아보니 Jasmine, Karma, JUnit, 또 최근 페이스북에서 만든 오픈소스 Jest 등 정말 종류가 다양하다. 그중 평소에 눈여겨 보았던 Mocha에 대해 다뤄보려 한다.


TDD?

 TDD(Test-driven development)에 대해 간단히 정리해 보자. 기존 개발 방식 Flow에 대해 생각해 보면 보통은 이런 방식으로 진행된다고 볼 수 있다. 

함수 설계 → 코드 작성 → 수동 테스트

 

 만약 새로운 기능이 추가된다거나 유지 보수를 위해서 코드를 수정하게 될 경우 위 Flow를 다시 따르게 되는데, 여기서 코드 규모가 커질 수록 비용이 기하급수적으로 커지는 부분이 있다. 바로 테스트 부분이다. 실제 완성된 프로그램에서 특정 부분을 고치는건 상당히 스트레스를 받는 일이다. 고친 부분이 잘 돌아가도 다른부분에서 내가 의도치 않은 부작용이 발생할 가능성이 있기 때문이다. 물론 프로그래머는 함수의 응집력을 높히고 결합도를 낮추는 방식으로 개발을 할 것이다. 최대한 의존성을 낮춰서 개발한다 하더라도 어쩔수 없이 영향을 받는 함수는 존재하기 마련이며, 이에 따라 어떤 Side-Effect가 발생할지 모두 고려해서 수정을 해야 한다. 만약 어플리케이션 규모가 작다면 고려해야할 부분도 적고, 이에 따라 수정에 의해 테스트 하는 시간도 적을 것이다. 하지만 기능이 다양한 대규모 어플리케이션에서는 작은 수정 만으로도 어떤 부작용이 발생할지 모두 예측하기가 어렵다. 따라서 내가 만든 코드가 의도한 결과를 산출해 내는지 보장해주는 것이 있다면 코드를 수정하는데 드는 비용을 최소한으로 줄일 수 있을 것이다.(예를 들면 테스트하는데 드는 시간, Side-Effect를 고려해 수정해야 하는 스트레스 등)


 여기게 착안한 개발 방식이 바로 테스트 주도 개발 방법론인 TDD이다. TDD Flow는 아래와 같다.


함수 설계 -> 테스트 코드 작성-> 코드 작성 

 

 기존 개발 방식과 다른점은 테스트 코드를 먼저 작성한다는 것이다. 함수 설계 후 의도한 결과를 바탕으로 테스트 코드를 작성한다. 이 후 테스트 코드가 의도한 결과가 나올 때 까지 수정 한 후, 완성된 테스트 코드를 바탕으로 실제 코드를 작성한다. 이렇게 개발하면 중간 산출물로 테스트 코드가 남게 되는데, 이후 리팩토링(Refactoring)등 소스코드 수정 후 수동으로 테스트 해볼 필요 없이 이전에 만든 테스트 코드를 돌려서 통과하는지만 체크하면 된다. 따라서 프로그래머는 부작용이 발생하더라도 적은 비용(그냥 테스트 코드만 쭉 돌려보면 되니까)으로 이를 탐지할 수 있기 때문에 테스트 비용을 크게 줄일 수 있다.


 말로 설명하면 잘 이해가 안될 수 있으니 실제로 만들어 보도록 하자. Mocha에 대한 간단한 사용법을 익힌 후 TDD 방법으로 한번 만들어 보도록 하자.

Install

 mocha는 node.js를 기반으로 돌아가는 프레임 워크이다. 따라서 npm을 이용하면 쉽게 설치할 수 있다. global로 설치해야 하므로 sudo 권한으로 실행하자.
$ sudo npm install mocha -g

Basic

 mocha는 기본적으로 Root Directory의 test폴더 안의 파일들을 대상으로 테스트를 실행한다. 그럼 test 폴더를 만든 후 test.js파일은 만들어 기본적인 코드를 작성해 보도록 하자. 
 사용법은 정말 간단하다. describe 함수를 사용해서 첫번째 인자는 테스트할 상황 묘사, 두번째 인자로 테스트할 코드를 callback으로 등록한다. it은 특정 상황에서 테스트할 코드의 내용과 callback을 넘겨준다. assert는 node.js에서 제공하는 기본 assertation 모듈이다. mocha에서는 shoude.js, expect.js등 다른 assertation을 사용할 수 있다. 

test/test.js
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

 비동기 함수도 쉽게 테스트할수 있다. 비동기 함수가 끝난 후 done() mehtod를 실행시켜 주자. 만약 에러가 발생한다면 done함수의 파라미터로 error 객체를 넘겨주면 된다.

test/asynchronous.js
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

 각각의 테스트 전 후에 해야하는 사전 작업들을 정의할 수 있다. before / after의 경우 describe 실행 전 한번만 실행되고, beforeEach, afterEach는 각각의 테스트가 실행될때 마다 작동한다.

test/hooks.js
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

 이제 이를 바탕으로 함수를 TDD방식으로 만들어 보도록 하자. 제일 먼저 만들 함수를 정의한다. 우리는 두 인자를 받아 더한 값을 반환해주는 Sum(x, y) 함수를 만들어 보자. 디테일한 Sum 함수의 조건은 다음과 같다.

  • 두 수를 입력 받아 결과 값을 리턴한다.
  • x, y는 number만 받을 수 있다.
  • 허용되지 않은 값이 들어올 경우 invalid String을 return 한다.
 자 이제 이를 바탕으로 테스트 케이스를 작성해 보도록 하자. it 함수를 사용해서 sum func가 성공했을 경우와 invalid 값이 들어왔을 경우 테스트 케이스를 정의한다. 앞에서 사용했던 assert.equal()을 이용해서 함수의 결과값이 내가 의도한 값과 같은지 아닌지를 판단하면 된다.

test/example.js

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


 모든 테스트 케이스를 통과 했다. 이제 내가 원하던 함수와 그 결과를 보장하는 테스트 케이스를 모두 얻었다. 추후 이 함수의 요구사항은 그대로 리팩토링을 했을 경우, 수정 후에 테스트 케이스를 돌린 후 제대로 통과하는지만 체크해 보면 될 것이다. 지금은 간단한 하나의 테스트 케이스만 작성했지만 조건이 많아질수록, 함수가 많아질수록 나중에 수정했을 경우의 리스크와 비용은 엄청나게 줄어들 것이다.


마치며

 사실 TDD에도 단점이 있다. 일반적인 개발 방법보다 속도가 느리다는 것이다. 실제로는 일정에 맞춰 개발해야하기 때문에 테스트 케이스를 만들며 개발하기란 쉽지가 않다. 그러나 한 조사 결과에 따르면 TDD로 개발할 경우 약 15% ~ 30%정도 개발 시간이 증가 하지만, 결함률은 40% ~ 90%로 크게 줄어 높은 품질의 코드를 유지할 수 있다고 한다. 따라서 TDD 방식으로 개발한다면 추후 유지보수에 들어가는 비용을 크게 줄일 수 있기 때문에, 길게 보자면 TDD가 더 이익일 것이다. 요즘은 스냅샷을 찍어서 비교해주는 것까지 가능하다고 하니 잘만 사용하면 코드 수정의 스트레스(?)에서 해방 될 수 있을 것이라 생각한다. (스냅샷 기능은 Jest에 있는 기능이다.) 여기서 소개한 방법은 기본적인 TDD 작성법이니 관심이 있는 사람은 구글에 검색해 보면 더 좋은 방법을 찾을 수 있을 것이다. (처음 TDD는 1999년에 eXtreme Programming과 같이 소개되었으니 꽤나 오래된 개발 방법이다.) 또 mocha 의외에 다양한 Test Framework가 존재하니 자신에게 맞는 툴을 찾아 사용해 보도록 하자.


'Language > JavaScript' 카테고리의 다른 글

Currying in Javascript  (0) 2017.07.23
Functional Programming in Javascript  (7) 2017.07.22
TDD & Mocha - Javascript Test Framework  (0) 2017.05.14
Javascript Image Filter - convolution  (0) 2017.05.02
React Server Side Rendering  (4) 2017.04.30
nodejs error handling  (0) 2017.04.23