본문 바로가기

node.js (OctoberSkyJs)

[Node.js 따라배우기] Node.js TDD 프레임워크 Expresso 매뉴얼 번역

따라하기 실습에서 사용하는 node.js 웹 프레임워크는 express 입니다. express는 왠만한 웹 애플리케이션 프레임워크 수준입니다. 보면 볼수록 괜찮다는 느낌이 드는 프레임워크인데요, 놀랍게도 firejune이라는 분께서 매뉴얼을 한글로 번역해 놓으셨습니다. Node.JS용 MVC 프레임워크 Express - 문서 번역. node.js관련해서 이런저런 사이트를 언급했었는데요, 이런 사이트도 있었는 줄 몰랐습니다. 이 외에도 이분의 블로그에는 node.js 관련된 좋은 내용이 참 많이 있습니다. 참고하시면 많은 도움 되실겁니다. 기회봐 여쭤봐서 오프라인 세미나때 초청해 볼 생각입니다. (잘 되야 할텐데 말입니다.ㅎㅎ) 

여튼 그에 필 받아서 저는 Express에서도 쓸수 있고, 우리의 따라배우기 강좌에도 나오는 node.js 테스트 프레임워크인 Expresso 매뉴얼을 번역해 봅니다.
지금은 그냥 한번 쭉~ 훓어 보시고, 따라배우기 스터디 도중 중간중간 필요하실때 참고 하시면 될 것 같습니다.  :D

참! 훅~딱~ 번역해서 오역/오타 있을수 있습니다. 눈에 띄시면 코멘트로 남겨주세요~ 



Expresso

원문: http://visionmedia.github.com/expresso/


Expresso
는 nodejs에서 사용하는 자바스크립트TDD 프레임워크입니다. . Expresso는 매우 빠르며, 기본 단정문 이외의 추가적인 단정문(assertion) 메소드와 코드 커버리지 레포트, CI 지원 등을 갖추고 있습니다. .

Features

  • 경량
  • 직관적인 비동기 지원 
  • 직관적인 테스트 러너 실행
  • node-jscoverage를 통한 테스트 커버리지 지원
  • 코어 assert module 사용과 확장지원
  • assert.deepEqual()의 별칭으로 assert.eql() 사용
  • assert.response() http response 유틸리티
  • assert.includes()
  • assert.isNull()
  • assert.isUndefined()
  • assert.isNotNull()
  • assert.isDefined()
  • assert.match()
  • assert.length()

설치 

expresso와 node-jscoverage를 설치하려면 다음과 같은 명령어를 사용한다. node-jscoverage가 먼저 컴파일 된다.

$ make install

커버리지 레포트 없이 expresso 단독으로 설치하려면 다음과 같이

$ make install-expresso

npm을 이용해서 설치하려면 아래와 같이

$ npm install expresso

예제들

테스트 케이스를 정의하기 위해 함수들을 export 합니다. 

exports['test String#length'] = function(){
    assert.equal(6, 'foobar'.length);
};

혹은, 다수의 테스트를 실행하기 위해서 테스트들을 포함하고 있는 객체를 export 하고 싶을 수도 있습니다. 

module.exports = {
    'test String#length': function(beforeExit, assert) {
      assert.equal(6, 'foobar'.length);
    }
};

괄호로 된 키 값을 설정하고 싶지 않다면 다음과 같이 합니다. 

exports.testsStringLength = function(beforeExit, assert) {
    assert.equal(6, 'foobar'.length);
};

콜백 함수로 전달 된 인자는 beforeExit와 assert입니다.각각의 테스트 함수에서 ("this")는 테스트할 객체를 지칭합니다.beforeExit 인자로 함수를 전달해서 테스트를 끝내기 전에 넘긴 해당 함수를 실행할 수 있습니다. 실제로 테스트가 잘 수행되었는지 확인할때 요긴합니다. beforeExit 는 this에 대한 exit 이벤트를 리스닝하는 단축 명령어에 해당합니다. 두번째 파리미터인 assert 는 테스트 대상 객체에 대한 assert 입니다. 비동기 콜백에서 단정문들이 올바른 테스트와 연결되는 것을 확실하게 만들어 줍니다.

exports.testAsync = function(beforeExit, assert) {
    var n = 0;
    setTimeout(function() {
        ++n;
        assert.ok(true);
    }, 200);
    setTimeout(function() {
        ++n;
        assert.ok(true);
    }, 200);

    // 테스트가 끝나면, exit 이벤트가 호출된다. 
    this.on('exit', function() {
        assert.equal(2, n, 'Ensure both timeouts are called');
    });

    // 대안으로, beforeExit를 단축명령어로 사용할 수 있다.
    beforeExit(function() {
        assert.equal(2, n, 'Ensure both timeouts are called');
    });
};

단정문 유틸리티들 Assert Utilities

assert.isNull(val[, msg])

val은 null이어야 한다!

assert.isNull(null);

assert.isNotNull(val[, msg])

valnull이 아니다!

assert.isNotNull(undefined);
assert.isNotNull(false);

assert.isUndefined(val[, msg])

val은 undefined 상태이어야 한다.

assert.isUndefined(undefined);

assert.isDefined(val[, msg])

val은 undefined상태가 아니어야 한다.

assert.isDefined(null);
assert.isDefined(false);

assert.match(str, regexp[, msg])

str이 정규표현식(regexp)과 일치해야 한다.

assert.match('foobar', /^foo(bar)?/);
assert.match('foo', /^foo(bar)?/);

assert.length(val, n[, msg])

val의 길이(length) n이어야 한다.

assert.length([1,2,3], 3);
assert.length('foo', 3);

assert.type(obj, type[, msg])

obj는 타입이 같아야 한다.

assert.type(3, 'number');

assert.eql(a, b[, msg])

객체 b는 객체 a와 같아야 한다. 코어 assert.deepEqual()메소드의 별칭에 해당한다. ==를 사용하는 assert.equal()와는 정 반대다. 

assert.eql('foo', 'foo');
assert.eql([1,2], [1,2]);
assert.eql({ foo: 'bar' }, { foo: 'bar' });

assert.includes(obj, val[, msg])

obj가 val을 포함해야 한다. 배열(Arrays)과 문자열(Strings)을 지원한다.

assert.includes([1,2,3], 3);
assert.includes('foobar', 'foo');
assert.includes('foobar', 'bar');

assert.response(server, req, res|fn[, msg|fn])

listen()을 호출하지 않고 주어진 server에서 단정문을 수행한다.  expresso에 의해 내부적으로 수행되고 모든 응답이 완료되면 서버가 중단된다. (listen을 하면 외부 입력을 받게 되므로 해당 메소드를 호출하지 않고 내부적으로 진행된다는 뜻) 이 메소드는 어떤 http.Server 인스턴스와도 동작하고, 따라서 Connectand 나 Express 서버들과도 잘 동작한다.

req 객체는 다음과 같은 것들을 담을 수 있다:

  • url: 요청 URL
  • timeout: 밀리초 단위의 타임아웃
  • method: HTTP 메소드 (GET/POST)
  • data: 요청 바디(request body)
  • headers: 헤더 객체


res 객체는 단정문을 위한 응답을 받는 콜백 함수가 될 수도 있고, 다음과 같은 프로퍼티를 가진 객체로써 응답(response)에 대해 몇몇 단정문을 실행하는 데에 쓰일 수도 있다.

  • body: 응답 몸체(response body)에 대한 단정문을 수행한다. 정규식이나 문자열이 될 수 있다.
  • status: 응답 상태코드(status code)에 대한 단정문을 수행한다. 
  • header: 헤더항목들의 일치여부를 확인한다. (지정되지 않은 헤더 항목은 무시. 정규식, 혹은 문자열 사용가능)


res를 넘길때 추가적인 단정문 처리를 위해 네번재 인자로 콜백함수를 넘길 수 있다. 

아래 코드는 위에서 설명한 내용에 대한 예제이다.

assert.response(server, {
    url: '/', timeout: 500
}, {
    body: 'foobar'
});

assert.response(server, {
    url: '/',
    method: 'GET'
}, {
    body: '{"name":"tj"}',
    status: 200,
    headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'X-Foo': 'bar'
    }
});

assert.response(server, {
    url: '/foo',
    method: 'POST',
    data: 'bar baz'
}, {
    body: '/foo bar baz',
    status: 200
}, 'Test POST');

assert.response(server, {
    url: '/foo',
    method: 'POST',
    data: 'bar baz'
}, {
    body: '/foo bar baz',
    status: 200
}, function(res){
    // 모두 완료. 필요하다면 추가적인 테스트 수행
});

assert.response(server, {
    url: '/'
}, function(res){
    assert.ok(res.body.indexOf('tj') >= 0, 'Test assert.response() callback');
});

response 단정문 함수는 응답이 없거나 타임아웃(기본 30초)이 발생하면 실패로 간주한다. 

expresso(1)

단일 테스트 스위트(=파일)을 실행하려면 다음과 같이

$ expresso test/a.test.js

몇몇 테스트 스위트들을 추가하길 원할 경우에는 다음과 같은 식으로 

$ expresso test/a.test.js test/b.test.js

모든 테스트 스위트 안에서 특정한 것들만을 지정해서 테스트 하고 싶다면, 즉 화이트리스트(whitelist)테스트를 하고 싶으면 다음과 같이 수행한다. 

$ expresso --only "foo()" --only "bar()"

혹은, 다음과 같은 식으로 한번에 호출도 가능하다.

$ expresso --only "foo(), bar()"

와일드카드 사용(Globbing)도 가능하다.

$ expresso test/*

아무것도 지정하지 않고 expresso를 호출하면 기본적으로는 test/* 를 호출한다. 따라서 아래 명령은 바로 위의 명령과 동일하다.

$ expresso

만약 테스트 실행전에 require.paths에서 지정된 패스를 변경하고 싶다면, -I (<=대문자 아이) 혹은 --include 옵션을 사용합니다. 

$ expresso --include lib test/*

앞선 예제는 제가 추천하는 전형적인 예제입니다. expresso는 expresso에 번들되어 있는 node-jscoverage 를 통한 테스트 커버리지를 지원합니다. (테스트 커버리지 까지!! 쏘~쿨~~!!) 사용하려면 라이브러리의 인스트루먼트(instrumented) 버전을 노출해야만 합니다. 
라이브러리를 인스트루먼트 하려면, 소스(src)와 대상(dest) 디렉터리를 node-jscoverage의 인자로 전달해 실행하면 됩니다.

$ node-jscoverage lib lib-cov

이제 다시 커버리지 구문들이 인스트루먼트된 lib-cov 디렉터리를 대상으로 테스트를 수행할 수 있습니다.

$ expresso -I lib-cov test/*

결과는 다음과 같을 겁니다. 물론 당신이 만든 테스트 커버리지에 따라 다르겠지요. :)

node coverage

이 일련의 작업을 좀 더 쉽게 만들기 위해 expresso는 -c 혹은 --cov 옵션을 갖고 있습니다. 위에서 말한 두 단계 명령어와 동일한 일을 수행합니다. (-_-) 다음 두 명령어는 동일한 테스트를 실행합니다. 하지만 하나는 자동으로 인스트루먼트 되고, lib-cov 경로로 변경해서 실행될 것이고, 다른 하나는 그냥 테스트만 실행할 겁니다. (커버리지 만드는 건 당연 아래쪽이겠죠?)

$ expresso -I lib test/*
$ expresso -I lib --cov test/*

현재 테스트 커버리지는 lib 디렉터리에 있습니다. 하지만 향후에 --cov 옵션에 패스를 지정하게 될 가능성이 급니다. 

만약 코드 커버리지 보고서를 자동 파싱하고 싶다면 --json [출력파일] 옵션을 사용하면 됩니다. 

$ expresso -I lib test/*
$ expresso -I lib --cov --json coverage.json test/*

json 구조로 만들어진 테스트 커버리지 보고서를 볼 수 있을 겁니다. 

{
    "LOC": 20,
    "SLOC": 7,
    "coverage": "71.43",
    "files": {
        "bar.js": {
            "LOC": 4,
            "SLOC": 2,
            "coverage": "100.00",
            "totalMisses": 0
        },
        "foo.js": {
            "LOC": 16,
            "SLOC": 5,
            "coverage": "60.00",
            "totalMisses": 2
        }
    },
    "totalMisses": 2
}

비동기 익스포트 (Async Exports)

콜백이나 특정 이벤트가 발생하기 전까지 테스트 수행을 지연시킬 필요가 있습니다.  exports.foo = function() {}; 구문이 이럴때 사용됩니다.

setTimeout(function() {
    exports['test async exports'] = function(){
        assert.ok('wahoo');
    };
}, 100);

익스포팅 시에 한 번 딱 실행됩니다. 해당 루핑내에서 첫번째 export 시점에 모든 테스트 함수들을 익스포트 해야 합니다.  이렇게하면 이미 export한 객체에 더이상 테스트를 추가할 수 없다는 뜻입니다.  (무슨 소린지..잘 이해가... -_-;;)



첨언!

중간에 서버를 테스트 하는 부분에 대한 테스트 코드 관련 이야기입니다.

우리 nodepad 예제에서 해당 테스트 코드를 쓰려면 물론 테스트 코드 로직 자체를 조금 수정해야 합니다. 하지만 그전에 먼저 해주어야 할 것이 있습니다. 예제에서 생략된 부분을 채우면 다음과 같은 식으로 server.test.js가 작성되어야 합니다.

== 이하는 nodepad 하위 디렉터리인 test 디렉터리에 위치하는 server.test.js 파일이라 가정 ==

var appserver = require('../app.js');

var server = appserver.app;


exports.testAsync = function(beforeExit, assert) {


assert.response(server, {

        url: '/', timeout: 500

}, {

...
...
(이하생략) 
 
그리고,  우리가 학습하고 있는 nodepad 앱의 경우 3000번 포트로 기동하지만, 가제 리스닝 명령어 때문에 서버 테스트 할때 리스닝상태로 남아있게 되는 수가 있습니다.


이런경우 app.js 파일에서 아래 부분처럼 리스닝 부분을 if로 감싸는 식으로 변경하면 됩니다. 

if (!module.parent) {

    app.listen(3000);

}

다른 모듈에 의해 포함될 경우 괄호안의 로직을 제외하는 코드입니다. 

Python 해보신 분들은 바로 아하! 하실겁니다
네 그렇습니다. 
python의 if __name__ == "__main__"  구문과 유사한 컵셉이라고 보시면 되겠습니다. :D