Vows

비동기(Asynchronous) 행위 주도 개발 for Node.

왜 우리에게 비동기 테스팅이 필요한지에 대한 두 가지 이유가 있습니다. 가장 명확한 첫 번째 이유는, node.js가 비동기이기 때문입니다. 따라서 테스트도 마찬가지여야 합니다. 두 번째 이유는, 동시에 실행함으로써 I/O 테스트를 더 빠르게 만들기 위해서입니다.

(*주의! IE 브라우저에서는 less.js가 정상동작하지 않아 스타일이 제대로 표시되지 않습니다.*)

vow를 작성한 다음, 실행하기:

$ vows test/* --spec

보고서를 출력해서 말 한대로 잘 지키고 있는지 확인하기

A non-promise return value
  ✓ should be converted to a promise
A topic not emitting an error
  ✓ should pass null if the test is expecting an errorshould pass the result otherwise
A topic emitting an error
  ✓ shouldn't raise an exception if the test expects it
A context with a nested context
  ✓ has access to the environment
  - can make coffee
A nested context
  ✓ should have access to the parent topics
A nested context with no topics
  ✓ should pass the parent topics downOK » 7 honored • 1 pending (0.112s)

시놉시스(Synopsis)

바우즈(Vows)는 Node.js행위주도개발(behavior driven development) 프레임워크입니다.

Vows는 비동기 코드를 테스트하기 위해 처음부터 다시 만들어졌습니다. 테스트가 해석이 되면 작성한 테스트를 병렬로 수행합니다. 그리고 의존관계가 있을 때에는 순차적으로 수행합니다.

실행 속도와 명료성, 그리고 사용자 경험에 중점을 두고 있습니다.

다음은 '0으로 나누기'를 기술한 간단한 예제입니다:

// division-by-zero-test.js

var vows = require('vows'),
    assert = require('assert');

// 테스트 스위트(Test Suite) 만들기
vows.describe('Division by Zero').addBatch({
    '숫자를 영으로 나누면': {
        topic: function () { return 42 / 0 },

        '무한대가 나온다': function (topic) {
            assert.equal (topic, Infinity);
        }
    },
    '하지만 0을 0으로 나누면': {
        topic: function () { return 0 / 0 },

        '우리가 얻게 되는 값은': {
            '숫자가 아니다.': function (topic) {
                assert.isNaN (topic);
            },
            '자기 자신과도 같지 않다.': function (topic) {
                assert.notEqual (topic, topic);
            }
        }
    }
}).run(); // 실행하기

실행 결과는:

$ node division-by-zero-test.js

자 이번엔, 좀 더 복잡한 예제입니다. 생성자 안에서 과일을 생성하는 ‘the-good-things’라 불리는 모듈이 있다고 가정해 보겠습니다.

exports.Strawberry = function () {
    this.color = '#ff0000';
};
exports.Strawberry.prototype = {
    isTasty: function () { return true }
};

exports.Banana = function () {
    this.color = '#fff333';
};
exports.Banana.prototype = {
    peel: function (callback) {
        process.nextTick(function () {
            callback(null, new(exports.PeeledBanana));
        });
    },
    peelSync: function () { return new(exports.PeeledBanana) }
};

exports.PeeledBanana = function () {};

the-good-things-test.js라는 파일에 다음과 같은 테스트 코드를 작성합니다.

var vows = require('vows'),
    assert = require('assert');

var theGoodThings = require('the-good-things');

var Strawberry   = theGoodThings.Strawberry,
    Banana       = theGoodThings.Banana,
    PeeledBanana = theGoodThings.PeeledBanana;

vows.describe('The Good Things').addBatch({
    '딸기는': {
        topic: new(Strawberry),

        '빨갛다': function (strawberry) {
            assert.equal (strawberry.color, '#ff0000');
        },
        '그리고 맛있다.': function (strawberry) {
            assert.isTrue (strawberry.isTasty());
        }
    },
    '바나나는': {
        topic: new(Banana),

        '껍질을 벗기면 *synchronously*': {
            topic: function (banana) {
                return banana.peelSync();
            },
            '`PeeledBanana`를 반환한다.': function (result) {
                assert.instanceOf (result, PeeledBanana);
            }
        },
        '껍질을 벗기면 *asynchronously*': {
            topic: function (banana) {
                banana.peel(this.callback);
            },
            '`PeeledBanana`를 반환한다.': function (err, result) {
                assert.instanceOf (result, PeeledBanana);
            }
        }
    }
}).export(module); // 테스트 스위트 익스포트(Export)

그리고 테스트 러너로 해당 테스트를 수행합니다.

$ vows the-good-things-test.js
혹은
$ vows --spec the-good-things-test.js 

설치(Installing)

Vows를 설치하는 가장 쉬운 길은 노드 패키지 매니저인 npm을 이용하는 것이다.

$ npm install vows

이렇게 하면 최신 안정화 버전을 얻게 된다. 만약 초최신 버전을 원한다면, 다음과 같이 실행한다.

$ npm install vows@latest

가이드(Guide)

Vows를 이해하기 위해, 테스트 작성에 연관된 각기 다른 컴포넌트들를 개략적으로 살펴보는 것 부터 시작할 생각이다. 그리고 나서 그 중 몇몇은 좀 더 자세히 살펴볼 생각이다.

테스트 스위트의 구조(Structure of a test suite)

Vows에서 가장 큰 테스트의 단위는 테스트 스위트(suites)이다. 관습적으로 하나의 테스트 스위트당 파일을 하나씩 만든다. 그리고 스위트의 이름과 파일이름을 일치시킨다. 테스트 스위트는 vows.describe로 만들어진다.

var suite = vows.describe('subject');

테스트들은 테스트 스위트의 일괄처리(batches) 안에 추가한다. addBatch 메소드가 이 작업을 한다.

suite.addBatch({});

하나의 스위트 안에 원하는 만큼의 일괄처리를 추가할 수 있다. 일괄처리 항목들은 순차적으로 수행된다.

suite.addBatch({/* 첫 번째 실행 */}).addBatch({/* 두 번 째 */}).addBatch({/* 세 번  */});

특정 순서로 기능동작을 테스트하길 원할 때, 일괄처리 체이닝이 유용하게 쓰인다.

일괄처리는 테스트 대상이 되는 각기 다른 컴포넌트들과 상태가 기술되어 있는 context(문맥)을 포함한다.

suite.addBatch({
   'A context': {},
   'Another context': {}
});

컨택스트들은 병렬로 실행되며 완전히 비동기적이다. 따라서 종료되는 시점은 불확정적이 된다.

컨택스트들은 보통 topics(주제)과 vows(서약)를 포함하며, 그것들의 조합으로 테스트가 정의된다.

suite.addBatch({
   '어떤 컨택스트(context)': {
        topic: function () {/* 비동기적으로 무언가를 수행 */},
        '이 부분이 vow': function (topic) {
            /* topic의 결과를 테스트 */
        }
    },
   '또 다른 컨택스트': {}
});

컨택스트는 하위-컨택스트를 가질 수 있다. 하위 컨택스트는 부모 컨택스트가 끝나자마자 실행된다.

suite.addBatch({
   '어떤 컨택스트': {
       topic: function () {/* 비동기적으로 무언가를 수행 */},
       '이 부분이 vow': function (topic) {
           /* topic의 결과를 테스트 */
       },
       '하위-컨택스트(sub-context)': {
           /* 위쪽 테스트 수행이 끝나면 실행된다. */
       }
   },
   '또 다른 컨택스트': {
       /*  '어떤 컨택스트'와 병렬로 수행됨 */
    }
});

요약(Summary)

» 하나의 suite는 0개 혹은 그 이상의 batch들을 가진 객체이며 실행되거나 익스포트(export)될 수 있다.

» 하나의 batch는 중첩된 context들의 구조를 나타내는 리터럴(literal) 객체이다.

» 하나의 context는 하나의 선택적인 topic, 0개 혹은 그 이상의 vows와 0개 혹은 그 이상의 sub-contexts(하위-컨택스트)로 구성된다.

» 하나의 topic은 하나의 값(value) 혹은 코드를 비동기적으로 실행할 수 있는 하나의 함수가 될 수 있다.

» 하나의 vow(서약)topic을 인자로 받는 함수로, topic을 대상으로 단정문(assertion)을 수행한다.

위 내용을 염두에 두면, 다음과 같은 문법을 상상할 수 있다.

SuiteBatch*
BatchContext*
ContextTopic? Vow* Context*

다음은 주석을 달아놓은 예제이다:

vows.describe('Array').addBatch({                      // Batch
    '어떤 배열이 ': {                                    // Context
        '3개의 element를 가진 배열일 경우': {              // Sub-Context
            topic: [1, 2, 3],                          // Topic

            'length가 3이 된다.': function (topic) {     // Vow
                assert.equal(topic.length, 3);
            }
        },
        '아무런 elements도 가지고 있지 않을 경우': {        // Sub-Context
            topic: [],                                 // Topic

            'length는 0이 되고': function (topic) {      // Vow
                assert.equal(topic.length, 0);
            },
            '`pop()`이 호출되었을 때, *undefined*를 반환한다.': function (topic) {
                assert.isUndefined(topic.pop());
            }
        }
    }
});

토픽 동작 방식(How topics work)

토픽(topic)을 이해하는 것이 Vows를 이해하기 위한 핵심 중 하나이다. 다른 테스팅 프레임워크들과 달리, Vows는 테스트 해야하는 요소인 topic과, 그리고 실제 테스트에 해당하는 vows을 확실히 분리하도록 강제한다.

간단한 컨택스트를 가진 예제로 시작해 보자:

{ topic: 42,
  '42와 같아야 한다': function (topic) {
    assert.equal (topic, 42);
  }
}

topic의 값이 테스트 함수의 인자로 전달되는 걸 볼 수 있다. (이 함수를 이제부터 vow(서약)로 부른다) 매우 간단하다. 자 이번엔, 다른 방식으로 작성된 동등한 예제를 살펴보자.:

{ topic: function () { return 42 },
  '42와 같아야 한다.': function (topic) {
    assert.equal (topic, 42);
  }
}

똑같다. Topic은 함수도 될 수 있다. 리턴 값이 topic이 된다. 자 그럼 여러 개의 vows가 있을 경우는 어떨까?

{ topic: function () { return 42 },
  '숫자이어야 한다.': function (topic) {
    assert.isNumber (topic);
  },
  '42와 같아야 한다.': function (topic) {
    assert.equal (topic, 42);
  }
}

예상대로 동작한다. 즉, 값이 각각의 vow로 전달된다. topic 함수가 딱 한 번만 수행된다는 점을 주의있게 보자.

스코프(Scope)

때로는 자식 topic 안에서 부모 topic의 값이 필요한 경우가 있다. topic에는 topic 스코프라는 개념이 있기 때문에 이 경우도 쉽게 할 수 있다. 예제를 살펴보자:

{ topic: new(DataStore),
  '`get()`과 `put()에 응답해야 한다.`': function (store) {
    assert.isFunction (store.get);
    assert.isFunction (store.put);
  },
  '`get(42)`를 호출하면 ': {
    topic: function (store) { return store.get(42) },
    '42라는 id값을 가진 객체를 반환해야 한다.': function (topic) {
      assert.equal (topic.id, 42);
    }
  }
}

위 예제에서, 최상위 레벨 topic의 값은 인자로 내부 topic으로 vows로 전달되는 것과 동일한 방식으로 전달된다. 확실히 하기 위해서, 난 최상위 topic을 참조하는 두 인자 모두에 store라고 이름을 붙였다.

유효범위는 한 레벨에만 한정되지 않는다. 다음과 같은 경우를 따져보자:

topic: function (a, /* 부모 topic             */
                 b, /* 부모의 부모 topic       */
                 c  /* 부모의 부모에 부모 topic */) {}

부모의 topic이 각각의 topic 함수에 순서대로 전달된다. 가장 근접한 부모는 첫번 째 인자 (a)이고 상위 부모는 (b, 그리고 c)가 된다. 마치 양파껍질처럼 말이다.

테스트 스위트 실행하기(Running a suite)

테스트 스위트를 실행하는 가장 쉬운 방법은 run메소드를 붙이는 것이다:

vows.describe('subject').addBatch({/* ... */}).run();

run 메소드는 선택적인 콜백을 갖는다. 그리고 그 콜백은 모든 테스트들의 실행이 끝났을 때 호출된다. 테스트 결과들은 (콜백이 존재할 경우) 다음과 같은 객체로 해당 콜백에 전달된다:

{ honored: 145,
  broken:    4,
  errored:   1,
  pending:   0,
  total:   150,
  time:  5.491
};

자 이제 우리가 테스트 스위트를 실행하고 싶고, 해당 스위트가 subject-test.js 안에 있다고 가정한다면, 그냥 다음처럼 하면 된다:

$ node subject-test.js

결과값이 기본 레포트인 'dot-matrix' 형태로 콘솔에 출력될 것이다.

테스트 스위트 내보내기(Exporting the suite)

테스트들이 점점 복잡해 지고, 여러개의 파일에 걸쳐서 만들어지게 될 때, 하나의 엔티티로 그 테스트들을 실행할 방법이 필요해 질 것이다.

Vows는 vows라 불리는 테스트 러너를 가지고 있으며, vows를 한 번에 여러개의 테스트 스위트를 실행하는데 쓸 수 있다. 그렇게 하려면, 작성한 테스트들을 바로 실행해 버리지 말고, 내보내야(export) 한다. 그렇게 만드는데는 몇 가지 방법이 있는데, 가장 쉬운 방법은 export메소드를 통하는 것이다.

// subject-test.js

vows.describe('subject').addBatch({/* ... */}).export(module);

export는 하나의 인자를 갖는다. 바로 내보내길 원하는 테스트 스위트에 해당하는 모듈이다. 다행히도, node는 takes one argument, the module you want to export the test suite to. Fortunately, module이라 불리는 글로벌 변수를 제공한다. 그리고 module변수는 현재 모듈을 가르킨다.

이제 테스트 파일을 실행하기 위해, 다음처럼 할 수 있다:

$ vows subject-test.js

결과는 직접 node로 실행했을 때랑 완전히 동일해야 한다. 차이점은 이제 다음처럼 실행할 수 있다는 점이다.

$ vows test/*

test/폴더 안에 들어 있는 모든 테스트들을 수행하고, 결과를 취합할 수 있다. 또한 vows에 옵션을 줄 수도 있다. 예를 들면, '스펙 스타일(spec style)'의 출력물을 얻기 위해서 --spec 플래그를 붙일 수 있다. 레퍼런스 섹션에는 붙일 수 있는 옵션에 대해 더 많이 나와 있으니 참고하자.

테스트 스위트들을 내보내는 다른 방법은 exports 객체에 해당 스위트들을 단순히 추가하는 것이다. API를 라이브러리도 내보는 방법과 동일한 방식이다.

exports.suite1 = vows.describe('suite one');
exports.suite2 = vows.describe('suite two');

자 그럼 요약해 보자!

// subject-test.js
// 'subject'라고 기술되는 테스트 스위트

vows.describe('subject') // 'subject'라는 이름이 붙은 테스트 스위트 생성
    .addBatch({})        // 1st batch 추가
    .addBatch({})        // 2nd batch 추가
    .addBatch({})        // 3rd batch 추가
    .export(module);     // 내보내기

비동기 테스트 작성

비동기 테스팅으로 뛰어들기 전에, topics 섹션을 확실히 읽어두길 바란다.

특정 파일의 존재 여부를 테스트하길 원한다고 가정해 보자. 그리고 몇 가지 경계조건(criteria)도 만족되어야 한다.

아시다시피, 비동기 함수로부터는 값을 '반환'받지 않는다. 값은 콜백 함수로 전달되어 버릴 뿐이다. 그럼 topics을 가지고 어떻게 테스트를 할 수 있을까? 다음을 살펴보자.

{ topic: function () {
    fs.stat('~/FILE', this.callback);
  },
  '접근 가능하다.': function (err, stat) {
    assert.isNull   (err);        // error 없음
    assert.isObject (stat);       // 상태 객체(stat object)가 있음
  },
  '비어있지 않다.': function (err, stat) {
    assert.isNotZero (stat.size); // 파일 size > 0
  }
}

여기에서의 핵심은 모든 topic들 안에서 사용가능한 특별한 ‘this.callback’ 함수이다

this.callback호출될 때, 마치 topic 함수에 의해 반환되는 값인 것마냥, 하나씩 하나씩, 테스트 함수의 인자로 값을 전달된다.

실질적으로, 이 방식을 통해 비동기 함수 호출로 부터 콜백을 분리시킬 수 있다.

이것이 바로 Vows가 모드 비동기적인 호출을 놓치지 않고 따라가며, 무언가 반환되지 않을 때는 경고를 하게 되는 방법이다.

this.callback’을 사용하는 topic은 아무것도 반환해서는 안된다. 마찬가지로, 아무것도 반환하지 않는 topic은 ‘this.callback’를 반드시 사용해야 한다.

약속들(Promises)

Vows는 또한 약속 기반의 비동기 아웃-오브-박스(외부 기능을 이용해 처리 하는 것)를 지원한다. 따라서 만약 목적에 더 잘 부합된다면, topic으로 부터 EventEmitter 인스턴스를 반환할 수 있다. 그리고 테스트는 "success" 혹은 "error" 이벤트가 발생할 때 수행될 것이다.

{ topic: function () {
    var promise = new(events.EventEmitter);

    fs.stat('~/FILE', function (e, res) {
        if (e) { promise.emit('error', e) }
        else   { promise.emit('success', res) }
    });
    return promise;
  },
  'can be accessed': function (err, stat) {
    assert.isNull   (err);        // error 없음
    assert.isObject (stat);       // 상태 객체(stat object)가 있음
  },
  'is not empty': function (err, stat) {
    assert.isNotZero (stat.size); // 파일 size > 0
  }
}

실행 순서와 병행론(Order of execution and parallelism)

우리는 어떻게 batch들과 컨택스트들이 실행되는지에 대해 짧막하게 이야기했었다. 하지만, 이젠 좀 더 자세히 깊이 파볼 시간이 되었다.

{ topic: function () {
    fs.stat('~/FILE', this.callback);
  },
  '`fs.stat`가 성공한 다음': {
    topic: function (stat) {
      fs.open('~/FILE', "r", stat.mode, this.callback);
    },
    '`fs.open`가 성공한 다음': {
      topic: function (fd, stat) {
        fs.read(fd, stat.size, 0, "utf8", this.callback);
      },
      '파일의 콘텐츠를 얻기 위해 `fs.read`를 실행할 수 있다.': function (data) {
        assert.isString (data);
      }
    }
  }
}

위 예제에서, 중첩된 콜백들을 복제하기 위한 중첩된 컨택스트들을 만들어 썼다. 보시다시피, 부모 topic의 결과는 인자로 자식에게 전달된다.

따라서, 전체적으로 이 예제는 비동기 처리가 남아 있는 동안에는 대부분 순차적으로 수행된다.


자 이젠 몇 몇 장치들을 체크하기 위해 병렬로 테스트 실행하는 예제를 살펴보자:

{ '/dev/stdout': {
    topic:    function () { path.exists('/dev/stdout', this.callback) },
    'exists': function (result) { assert.isTrue(result) }
  },
  '/dev/tty': {
    topic:    function () { path.exists('/dev/tty', this.callback) },
    'exists': function (result) { assert.isTrue(result) }
  },
  '/dev/null': {
    topic:    function () { path.exists('/dev/null', this.callback) },
    'exists': function (result) { assert.isTrue(result) }
  }
}

이 경우엔, 테스트들이 순서 없이 끝이 난다. 서로간에 의존성이 있어서는 안된다. 테스트 스위트는 마지막 I/O 콜이 완료될 때 빠져 나오게 되고, 해당 스위트에 대한 단정문(assertions)들이 수행된다.

말하자면, 중첩된 컨택스트(nested contexts)들이 순차적으로 실행되는 동안에 형제뻘 되는 컨택스트(sibling contexts)들이 병렬로 수행된다는 얘기다. 이 모든게 비동기적으로 일어난다는 점에 주목해야 한다. 따라서 몇몇 컨택스트들이 부모 컨택스트가 끝나기를 기다리는 동안, 근접한 컨택스트들은 그 사이에 실행될 수 있다.

단정문들(Assertions)

Vows는 node에 번들되어 있는 단정문(assertion)을 유용한 함수들로 확장하고 좀 더 나은 에러 리포트를 만들어 낸다.

값을 테스트 할 때는 보다 더 구체적인 단정문 함수를 사용하는 게 제일 좋다. 그렇게 하면, 의도가 훨씬 명확해지기 때문에, 더 나은 에러 리포트를 얻을 수 있게 될 것이다.

다음과 같은 배열을 가지고 있다고 해보자:

var ary = [1, 2, 3];

그리고 5개의 요소를 가지고 있다는 단정문을 만들어 보자. 내장된 assert.equal를 사용해 다음과 할 수 있다.

assert.equal(ary.length, 5);

실행하면 다음과 같은 에러가 나온다.

expected 5, got 3

자 이번에 우리가 쓸 수 있는, 좀 더 구체적인 단정문 함수 assert.length를 써보자.

assert.length(ary, 5);

다음과 같은 에러를 리포트 한다.

expected [1, 2, 3] to have 5 elements

assert.match, assert.instanceOf, assert.include and assert.isEmpty등등을 비롯한 다른 유용한 단정문 함수들이 vows에 번들되어 있다. 전체 목록을 보고 싶으면 reference를 참조한다.

매크로(Macros)

때로는, 테스트 스위트를 통해 사용되는 테스트들을 추상화하는 것이 유용할 때가 있다. Vows에서 batch는 엄밀히 말하자면 데이터 구조의 일종인 트리(tree) 객체다. 이건, 앞으로 보게되겠지만 꽤 강력한 구조라는 것이 증명되었다.

내가 작성하는 대다수의 코드에서 내가 테스트 해야만 하는 것은 HTTP 상태 코드다. 우선, 어떤 작업인지 곧장 한번 살펴보자. 아래는 비동기 클라이언트(client) 라이브러리다.

{ topic: function () {
    client.get('/resources/42', this.callback);
  },
  '200 OK로 응답해야 한다.': function (e, res) {
    assert.equal (res.status, 200);
  }
}

나쁘진 않다. 하지만, API에 대해서 테스트를 한다면, 이런 코드가 수백개가 있을 수 있다. 이런 경우, 매크로를 이용하면 어떻게 할 수 있는지 살펴보자.

function assertStatus(code) {
    return function (e, res) {
        assert.equal (res.status, code);
    };
}

상태 코드를 받아서는, 해당 상태 코드를 테스트 하는 함수를 돌려주는 코드이다. 이제 우리의 테스트를 다음과 같이 개선할 수 있다.

{ topic: function () {
    client.get('/resources/42', this.callback);
  },
  '200 OK로 응답해야 한다.': assertStatus(200)
}

훨씬 나아졌다. 그럼 topic은 어떻게 할까? API 호출용 매크로를 작성해 보자.:

var api = {
    get: function (path) {
        return function () {
            client.get(path, this.callback);
        };
    }
};

그리고, 우리의 테스트를 재작성 해보면 :

{ topic: api.get('/resources/42'),
  'should respond with a 200 OK': assertStatus(200)
}

끝내준다. 다음은 이 매크로를 쓰면 어떻게 되는지 보여주는 예제이다.:

{   'GET /': {
        topic: api.get('/'),
        '200 OK로 응답해야 한다.': assertStatus(200)
    },
    'POST /': {
        topic: api.post('/'),
        '405 Method not allowed로 응답해야 한다.': assertStatus(405)
    },
    'GET /resources (no api-key)': {
        topic: api.get('/resources'),
        '403 Forbidden으로 응답해야 한다.': assertStatus(403)
    },
    'GET /resources?apikey=af816e859c249fe'
        topic: api.get('/resources?apikey=af816e859c249fe'),
        '200 OK를 반환해야 한다.': assertStatus(200),
        '리소스 리스트를 반환해야 한다.': function (res) {
            assert.isArray (res.body);
        }
    }
}

위 코드에서, 좀 더 진행해 볼 수 있을까? 당근 가능하다. 지금이 바로 정말 흥미로운 시점이다. 어떻게하면 문맥에 맞는 테스트들을을 생성해 낼 수 있는 보여주겠다.

topic을 만들어 내는 함수와, vow를 만드는 함수를 각각 따로 만들어 내는 대신에, topic과 vow를 동시에 포함하는 context를 만들어내는 함수를 만들 생각이다.

Topic은 문맥적인 요청을 수행할 것이다. 이 부분이 흥미로운 부분이다. api 요청을 만들어 내기 위한 context 설명(description)을 파싱할 것이다. 테스트는 설명문 안에 인코딩 될 거다. 자, 어떤 구현이 가능할지 한번 살펴보자.

//
// 요청을 보내고 응답코드를 확인한다.
//
function respondsWith(status) {
    var context = {
        topic: function () {
            // 현재 컨택스트의 이름을 가져온다. 예를 들면 "POST /" 같은.
            // 그런다음 공백으로 분리한다.
            var req    = this.context.name.split(/ +/), // ["POST", "/"]
                method = name[0].toLowerCase(),         // "post"
                path   = name[1];                       // "/"

            // 위 코드의 method와 path를 이용해서
            // 문맥상의 클라언트 요청을 수행한다. 
            client[method](path, this.callback);
        }
    };
    // vow를 생성해서 context에 할당한다.
    // 설명은 예상 상태 코드와 상태 이름
    // 그리고 node의 http 모듈로 부터 생성해 낸다.
    context['should respond with a ' + status + ' '
           + http.STATUS_CODES[status]] = assertStatus(status);

    return context;
}

자, 이제 앞에 작성했던 batch의 context 세 개를 재 작성해 보면 다음과 같이 된다.

{ 'GET  /':                   respondsWith(200),
  'POST /':                   respondsWith(405),
  'GET  /resources (no key)': respondsWith(403)
}

그리고 실행하면, 다음과 같은 결과를 얻는다.

GET  /
  ✓ should respond with a 200 OK
POST /
  ✓ should respond with a 405 Method Not Allowed
GET  /resources (no key)
  ✓ should respond with a 403 Forbidden

네 번째 컨택스트는 두 개의 vows를 가지고 있어서 조금 더 복잡하지만, 한번 직접 해보는 숙제로 남겨놓기로 한다.

레퍼런스(Reference)

명령행 인터페이스(CLI)와 단정문(assertion) 모듈은 아래에 문서로 만들어 놓았다.

테스트 러너(Test runner)

vows [FILE, ...] [options]

특정 테스트들 실행하기

$ vows test-1.js test-2.js
$ vows tests/*

test/ 혹은 spec/ 폴더 아래에 있는 모든 테스트 수행하기

$ vows

Watch mode

$ vows -w
$ vows --watch

옵션들(Options)

-v, --verbose Verbose mode
-w, --watch Watch mode
-m STRING String matching: Only run tests with STRING in their title
-r REGEXP Regexp matching: Only run tests with REGEXP in their title
--json Use JSON reporter
--spec Use Spec reporter
--dot-matrix Use Dot-Matrix reporter
--version Show version
-s, --silent Don't report
--help Show help

Assertion functions

equality

assert.equal          (4, 4);
assert.strictEqual    (4 > 2, true);

assert.notEqual       (4, 2);
assert.strictNotEqual (1, true);

assert.deepEqual      ([4, 2], [4, 2]);
assert.notDeepEqual   ([4, 2], [2, 4]);

type

assert.isFunction (function () {});
assert.isObject   ({goo:true});
assert.isString   ('goo');
assert.isArray    ([4, 2]);
assert.isNumber   (42);
assert.isBoolean  (true);

assert.typeOf     (42, 'number');
assert.instanceOf ([], Array);

truth

assert.isTrue  (true);
assert.isFalse (false);

null, undefined, NaN

assert.isNull      (null);
assert.isNotNull   (undefined);

assert.isUndefined ('goo'[9]);
assert.isNaN       (0/0);

inclusion

assert.include ([4, 2, 0], 2);
assert.include ({goo:true}, 'goo');
assert.include ('goo', 'o');

regexp matching

assert.match ('hello', /^[a-z]+/);

length

assert.length ([4, 2, 0], 3);
assert.length ('goo', 3);

emptiness

assert.isEmpty ([]);
assert.isEmpty ({});
assert.isEmpty ("");

exceptions

assert.throws(function () { x + x }, ReferenceError);
assert.doesNotThrow(function () { 1 + 1 }, Error);

About

Vows는 Alexis Sellier에 의해 개발되었으며, 그는 cloudhead로 더 잘 알려져 있다.

한글 번역은 doortts가 하였으며, 그는 너구리라는 별명으로 불리고 있다. (으응?)

Fork me on GitHub