Introduction

Last updated 16 days ago

Kava has been powering accurate test suites for Node.js and Web Browsers since 2012.

What is Kava?

Kava is a npm package that provides your Node.js and Web Browser code with test suite capabilities.

Outside of development, kava is a narcotic sedative drink made in Polynesia from the crushed roots of a plant of the pepper family.

https://en.oxforddictionaries.com/definition/kava

Why Kava?

Kava is built upon TaskGroup which offers all the same benefits as the other testing libraries, but with these superior benefits:

  1. It accurately associates tests with suites. This is something which other test utilities merely guess, which leads them to false positives, especially with asynchronous tests.

  2. It doesn't need a special runtime setup. Your tests can be run just like any other script you write. Allowing you to use your existing knowledge to write and debug your tests.

In general, it can be stated that kava produces a state of calmness, relaxation and well-being without diminishing cognitive performance.

https://en.wikipedia.org/wiki/Kava

Getting Started

Installation

To make use of Kava for your testing, you must first install it:

npm install --save-dev kava

Usage

Using Kava is as simple as requiring it.

Let's say we are writing a package called myfirstpackage, generally we would write a test.js file which makes use of Kava like so:

test.js
const kava = require('kava')
kava.suite('myfirstpackage', function (suite, test) {
suite('a group of tests', function (suite, test) {
test('an individual test', function () {
/* the testing logic for this test would go here */
})
})
})

To run our tests with Kava, we simply run our tests:

node ./test.js

Which would produce the following:

myfirstpackage
myfirstpackage ➞ a group of tests
myfirstpackage ➞ a group of tests ➞ an individual test
myfirstpackage ➞ a group of tests ➞ an individual test ✔
myfirstpackage ➞ a group of tests ✔
myfirstpackage ✔
1/1 tests ran successfully, everything passed

Architecture

Kava under the hood is powered by the robust TaskGroup flow control library.

Tests in Kava are extensions of the TaskGroup's Task class. Suites in Kava (a collection of tests) are extensions of TaskGroup's TaskGroup class.

This makes the Kava codebase incredibly simple yet incredibly powerful thanks to the existing robustness of TaskGroup, which already offers flow control, error reporting, and optional completion callbacks. All Kava needs to add, is the syntactic sugar and reporting that we come to expect from a modern testing framework.

Tests

Tests can be written like so:

test('my synchronous test name', function () {
console.log('running our test')
})

Which will output:

my synchronous test name
running our test
my synchronous test name ✔
1/1 tests ran successfully, everything passed

Tests can also become asynchronous by accepting a completion callback like so:

test('my asynchronous test', function (done) {
console.log('starting test...')
setTimeout(function () {
console.log('...finishing test')
done()
}, 1000)
})

Which will output:

my asynchronous test name
starting test...
...finishing test
my asynchronous test name ✔
1/1 tests ran successfully, everything passed

Test Examples

Here is an example of a synchronous read file test:

const file = __filename
test('read the current file and assert it contains the word test', function () {
// if this throws for whatever reason, the error will be caught by Kava, and the test marked as failed
if ( require('fs').readFileSync(file).indexOf('test') === -1 ) {
// errors can also be returned rather than thrown
return new Error(`could not find the word test within the file ${file}`)
}
})

Here is an example of an asynchronous read file test:

const file = __filename
test('reading the current file is possible', function (done) {
console.log('commencing read of', file)
require('fs').readFile(file, function (err, data) {
// if there was an error, let your completion callback know
if ( err ) return done(err)
// if there wasn't an error, let us know
console.log('file read successfully')
// call our completion callback to signify this test is now complete
done()
})
})

Suites

A suite is a collection of tests. They can be written like so:

suite('main suite', function (suite, test) {
test('async test within main suite', function (done) {
console.log(' starting [async test within main suite]...')
setTimeout(function () {
console.log(' ...finishing [async test within main suite]')
done()
}, 1000)
})
test('sync test within main suite', function () {
console.log(' running [sync test within main suite]')
})
// create a child suite within the main suite
suite('child suite', function (suite, test) {
test('async test within child suite', function (done) {
console.log(' starting [async test within child suite]...')
setTimeout(function () {
console.log(' ...finishing [async test within child suite]')
done()
}, 1000)
})
test('sync test within child suite', function () {
console.log(' running [sync test within child suite]')
})
})
})

The receiving of the suite and test arguments, and their usage within, creates proper nesting structures, such that children are actually children and are treated as such, in regards to reporting and flow execution, as seen by the output of the above:

main suite
main suite ➞ async test within main suite
starting [async test within main suite]......
...finishing [async test within main suite]
main suite ➞ async test within main suite ✔
main suite ➞ sync test within main suite
running [sync test within main suite]
main suite ➞ sync test within main suite ✔
main suite ➞ child suite
main suite ➞ child suite ➞ test within child suite
starting [async test within child suite]......
...finishing [async test within child suite]
main suite ➞ child suite ➞ async test within child suite ✔
main suite ➞ child suite ➞ sync test within main suite
running [sync test within child suite]
main suite ➞ child suite ➞ sync test within child suite ✔
main suite ➞ child suite ✔
main suite ✔
4/4 tests ran successfully, everything passed

As you would notice from the above, by default Suites execute their items serially, that is one after the other, waiting for the current item to complete before starting the next. This is a sensible default, and is inherited from Suites inheritance of TaskGroup.

Suite Concurrency

As Suites do inherit from TaskGroup, and as TaskGroup allows us to configure the concurrency via the concurrency option, we could have our suite execute in parallel by doing:

suite('main suite', function (suite, test) {
// make this suite execute in parallel with unlimited concurrency
this.setConfig({concurrency: 0})
// continue just like before
// ...
})

Which will alter the output to be the following:

main suite
main suite ➞ async test within main suite
starting [async test within main suite]......
main suite ➞ sync test within main suite
running [sync test within main suite]
main suite ➞ sync test within main suite ✔
main suite ➞ child suite
main suite ➞ child suite ➞ test within child suite
starting [async test within child suite]......
...finishing [async test within child suite]
main suite ➞ child suite ➞ async test within child suite ✔
main suite ➞ child suite ➞ sync test within main suite
running [sync test within child suite]
main suite ➞ child suite ➞ sync test within child suite ✔
main suite ➞ child suite ✔
...finishing [async test within main suite]
main suite ➞ async test within main suite ✔
main suite ✔
4/4 tests ran successfully, everything passed

Suite Initialisation

Suites can also create tasks asynchronously by the use of a completion callback for the suite initialiser.

suite('main suite', function (suite, test, done) {
// add some tests immediately just like before
test('this test is added to the suite immediately, just like in the previous example', function () {
//code for your test
})
// add some tests after a 1 second delay
setTimeout(function () {
test('this test is added to the suite after a 1 second delay', function () {
// code for your test
})
suite('this suite is added to the suite after a 1 second delay', function (suite, test) {
test('some test', function () {
// code for your test
})
})
// call our suites completion callback to signify the suite is now ready to run
done()
}, 1000)
})

This is because under the hood, the suite initialiser is just a TaskGroup Task, that Kava configures with the suite and test arguments, and the TaskGroup pops on the optional completion callback and executes it accordingly.

Suite Example

Here is an example making use of all that we learned above:

// Prepare
const fsUtil = require('fs')
const pathUtil = require('path')
const dir = __dirname
// Create our suite, that will read the directory, and add tests for file
suite(`all files within ${dir} contain the word test`, function (suite, test, complete) {
// Make the file tests occur in parallel to save time
this.setConfig({concurrency: 0})
// Add the file tests by reading the directory
fs.readdir(dir, function (err, _files) {
// Abort the test if there was an error
if ( err ) return complete(err)
// Add the tests for each file
files.forEach(function (file) {
const path = pathUtil.join(dir, file)
// Add the test for reading and checking the file
test(`checking ${file}`, function (complete) {
fs.readFile(path, function (err, data) {
if ( err ) return complete(err)
if ( data.toString().indexOf('test') === -1 ) {
return complete(new Error('could not find the word test'))
}
return complete()
})
})
})
// Directory has been read, tests have been added, signal our initialisation is complete
// so that the items within our suite can be run
return complete()
})
})

Hooks

Kava gives us support for before and after hooks for our tests and suites, such that we can perform synchronous and asynchronous actions before or after a specific test or suite, or all tests or suites within a particular suite.

To add before and after hooks to a specific Suite or Test, we would pass our listeners into the before and after options like so:

function before (complete) {
console.log('this is running before the test:', this.name)
setTimeout(function () {
console.log('and has a completion callback making it asynchronous')
complete()
}, 1000)
}
function after () {
console.log('this is running after the test:', this.name)
}
kava.test('our test with hooks', {before, after}, function (suite, test) {
console.log('our test is running')
})

To add before and after hooks for every item within a suite, we can use event listeners like so:

kava.suite('our suite', function (suite, test) {
this.on('test.before', function (test, complete) {
console.log('this is running before the test:', test.name)
setTimeout(function () {
console.log('and has a completion callback making it asynchronous')
complete()
}, 1000)
})
this.on('test.after', function (test, complete) {
console.log('this is running after the test:', test.name)
})
test('first test', function (complete) {
setTimeout(function () {
console.log('this is the output from the first test')
complete()
}, 1000)
})
test('second test', function () {
console.log('this is the output from the second test')
})
})

Reporters

Kava also supports different reporters. Reporters listen to the status of suites and tests and output their results to you.

The default reporter is the Console reporter. However, there is another builtin reporter called the List reporter which outputs only the results of tests, rather than their commencement.

You can select the reporter you wish to use via the KAVA_REPORTER environment variable like so:

env KAVA_REPORTER=list node ./test.js

Summary

This page covers everything you need to utilise Kava for your testing purposes.