Vows
Asynchronous BDD & continuous testing for node.js
Install / Use
/learn @vowsjs/VowsREADME
vows
vows is a testing framework for NodeJS.
License
- Copyright 2016-2018 fuzzy.ai mailto:legal@fuzzy.ai
- Copyright 2017 AJ Jordan mailto:alex@strugee.net
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Example
var vows = require('vows');
// vows does not pollute the assert module namespace by default
var assert = vows.assert;
vows
.describe("My first vows test")
.addBatch({
'When we open a file': {
topic: function() {
fs.open("/tmp/fakefile", "w", this.callback);
},
'it works': function(err, fd) {
assert.ifError(err);
assert.isNumber(fd);
},
teardown: function(fd) {
fs.close(fd, this.callback);
}
'and we write to the file': {
topic: function(fd) {
fs.write(fd, "My dog has fleas\n", this.callback);
},
'it works': function(err, written, buffer) {
assert.ifError(err);
assert.greater(written, 0);
assert.isString(buffer);
}
}
}
})
.run();
Introduction
Requiring
You require the module like any other module.
Assert macros
vows provides its own suite of assert macros. To use them, you should use the
assert property from the vows module, like so:
var vows = require('vows');
var assert = vows.assert;
Data structures
The basic way to use tests is to build really large hierarchical objects with a particular well-defined form.
Batch
For vows, the core concept is the test batch. A batch is an object that
consists of the following:
-
A
topicfunction that generates values to be tested -
One or more test functions, which accept the results of the
topicand use assert macros to validate the results -
Zero or more sub-batches
-
An optional
teardownfunction that cleans up any values generated by the topic
A batch can be either synchronous or asynchronous. For a synchronous batch,
the topic function just returns a value, and the test functions measure that
value:
let batch = {
"We get the answer": {
topic() {
return 6 * 7;
},
"it equals 42": (err, answer) => {
assert.ifError(err);
assert.equal(answer, 42);
}
}
};
For an asynchronous batch, the topic returns its results through the callback
property of this. vows knows that the callback will be used because the
result returned by the topic function is undefined.
let batch = {
"When we get the answer asynchronously": {
topic() {
setImmediate(() => {
this.callback(null, 6 * 7);
});
return undefined;
},
"it equals 42": (err, answer) => {
assert.ifError(err);
assert.equal(answer, 42);
}
}
};
Alternately, a topic can return a Promise.
vows will resolve the returned Promise and call tests with the same
(err, results) format as with other types of call.
let batch = {
"When we get the answer": {
topic() {
return new Promise((resolve, reject) => {
fs.open("/tmp/testfile", "w", (err, fd) => {
if (err) {
reject(err);
} else {
resolve(fd);
}
})
});
},
"it equals 42": (err, fd) => {
assert.ifError(err);
assert.isNumber(fd);
}
}
};
Note that all test functions receive at least an err argument, and then one or
more arguments. Synchronous batches can only have one test argument;
asynchronous batches can have a lot.
For backwards compatibility, it's possible to call this.callback synchronously
in your topic. vows will simply call setImmediate to call the callback
later. But that is a tricky and confusing way to write your tests, and you
should probably avoid it.
A batch can also have sub-batches. These are just properties of the batch that
are also batch objects, with their own topic, tests, sub-batches, teardown,
etc. The argument to the topic will be the results of the parent batch, in
reverse order up the hierarchy.
let batch = {
"When we get the answer": {
topic() {
return 6 * 7;
},
"it equals 42": (err, answer) => {
assert.ifError(err);
assert.isNumber(answer);
assert.equal(answer, 42);
},
"and we ask a couple of questions": {
topic(answer) {
return [
"What is six times seven?",
"How many roads must a person walk down?"
];
},
"they look plausible": (err, questions) => {
assert.ifError(err);
assert.isString(question[0]);
assert.equal(question[0][question[0].length - 1], '?');
assert.isString(question[1]);
assert.equal(question[1][question[1].length - 1], '?');
},
"and we compare the answer and the question": {
topic(questions, answer) {
setImmediate(() => {
this.callback(null, questions[0], questions[1], answer);
});
return undefined;
},
"they match up well": (err, question0, question1, answer) => {
assert.ifError(err);
// NB: you need to implement isAnswerTo yourself
assert(isAnswerTo(answer, question0));
assert(isAnswerTo(answer, question1));
}
}
}
}
};
Note that if a batch's topic returns more than one value to its callback, they
will be provided in order for any sub-batches' topic, but hierarchically
in reverse order. This may be a little confusing.
Note also that if an error occurs, in either the topic or the tests, the sub-batches will not be run.
The teardown method is called after all the tests and sub-batches have been
run. So, the order is something like this:
- topic
- tests
- sub-batches (if there are no errors)
- teardown
The teardown gets the non-error results of the topic as arguments. It's
useful for cleaning up things that the topic made a mess of.
batch = {
'When we open a file': {
topic: function() {
fs.open("/tmp/fakefile", "w", this.callback);
},
'it works': function(err, fd) {
assert.ifError(err);
assert.isNumber(fd);
},
teardown: function(fd) {
fs.close(fd, this.callback);
}
}
};
teardown functions can also be synchronous or asynchronous, or they can return
a Promise. However, the results are ignored.
let batch = {
"When we get the answer": {
topic() {
return new Promise((resolve, reject) => {
fs.open("/tmp/testfile", "w", (err, fd) => {
if (err) {
reject(err);
} else {
resolve(fd);
}
})
});
},
"it equals 42": (err, fd) => {
assert.ifError(err);
assert.isNumber(fd);
},
teardown(fd) {
return new Promise((resolve, reject) => {
if (typeof(fd) != 'number') {
reject(new Error("File descriptor is not a number"));
} else {
fs.close(fd, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
}
});
}
}
};
Note that the teardown will be called regardless of whether errors happened or not, so it's a good idea to check the arguments to make sure they're valid.
Teardowns are called as soon as the batch finishes; this is different from how vows.js works, but it is better.
If you're using a version of node that can handle async/await syntax, (>= 7.10.1), you can use async functions in your topics and teardowns, which can make your aysnchronous test code about as lovely and compact as can be.
const fs = require('fs');
const util = require('util');
// util.promisify is available in node > 8.0.0
const open = util.promisify(fs.open);
const close = util.promisify(fs.close);
let batch = {
"When we get the answer": {
topic: async function () {
return await open("/tmp/testfile", "w");
},
"it equals 42": (err, fd) => {
assert.ifError(err);
assert.isNumber(fd);
},
teardown: async function (fd) {
return await close(fd);
}
}
};
Suite
Batches are organized into suites. You create a suite with the describe method
of vows.
const vows = require('vows');
let suite = vows.describe('A new suite');
You can then add one or more batches to the suite using the addBatch method.
suite.addBatch(batch1);
suite.addBatch(batch2);
suite.addBatch(batch3);
Finally, you have two options to actually run the test suite. The first is the
aptly-named run() method, which runs all the tests and reports the results to
stdout. You can then run the script through node and you'll run all your
tests.
Alternately, you can use the export() method, passing the current module as
an argument. This will change the exports property of the module to be the
run() method of the suite. In other words, the module will now export a single
function that runs the suite.
The vows command-line tool can be used to run all your test modules that
use export().
./node_modules/.bin/vows test/*.js
All the suite methods are [chainable](https://en.wikipedia.org
