Because we have several Notes models, the test suite should run against any model. We can write tests using the Notes model API we developed, and an environment variable should be used to declare the model to test.
Because we've written the Notes application using ES6 modules, we have a small challenge to overcome. Mocha only supports running tests in CommonJS modules, and Node.js (as of this writing) does not support loading an ES6 module from a CommonJS module. An ES6 module can use import to load a CommonJS module, but a CommonJS module cannot use require to load an ES6 module. There are various technical reasons behind this, the bottom line is that we're limited in this way.
Because Mocha requires that tests be CommonJS modules, we're in the position of having to load an ES6 module into a CommonJS module. A module, esm, exists which allows that combination to work. If you'll refer back, we installed that module in the previous section. Let's see how to use it.
In the test directory, create a file named test-model.js containing this as the outer shell of the test suite:
'use strict';
require = require("esm")(module,{"esm":"js"});
const assert = require('chai').assert;
const model = require('../models/notes'); describe("Model Test", function() { .. });
The support to load ES6 modules is enabled by the require('esm') statement shown here. It replaces the standard require function with one from the esm module. That parameter list at the end enables the feature to load ES6 modules in a CommonJS module. Once you've done this, your CommonJS module can load an ES6 module as evidenced by require('../models/notes') a couple of lines later.
The Chai library supports three flavors of assertions. We're using the assert style here, but it's easy to use a different style if you prefer. For the other styles supported by Chai, see http://chaijs.com/guide/styles/.
Chai's assertions include a very long list of useful assertion functions, see http://chaijs.com/api/assert/.
The Notes model to test must be selected with the NOTES_MODEL environment variable. For the models that also consult environment variables, we'll need to supply that configuration as well.
With Mocha, a test suite is contained within a describe block. The first argument is descriptive text, which you use to tailor the presentation of test results.
Rather than maintaining a separate test database, we can create one on the fly while executing tests. Mocha has what are called hooks, which are functions executed before or after test case execution. The hook functions let you, the test suite author, set up and tear down required conditions for the test suite to operate as desired. For example, to create a test database with known test content:
describe("Model Test", function() {
beforeEach(async function() {
try {
const keyz = await model.keylist();
for (let key of keyz) {
await model.destroy(key);
}
await model.create("n1", "Note 1", "Note 1");
await model.create("n2", "Note 2", "Note 2");
await model.create("n3", "Note 3", "Note 3");
} catch (e) {
console.error(e);
throw e;
}
});
..
});
This defines a beforeEach hook, which is executed before every test case. The other hooks are before, after, beforeEach, and afterEach. The each hooks are triggered before or after each test case execution.
This is meant to be a cleanup/preparation step before every test. It uses our Notes API to first delete all notes from the database (if any) and then create a set of new notes with known characteristics. This technique simplifies tests by ensuring that we have known conditions to test against.
We also have a side effect of testing the model.keylist and model.create methods.
In Mocha, a series of test cases are encapsulated with a describe block, and written using an it block. The describe block is meant to describe that group of tests, and the it block is for checking assertions on a specific aspect of the thing being tested. You can nest the describe blocks as deeply as you like:
describe("check keylist", function() {
it("should have three entries", async function() {
const keyz = await model.keylist();
assert.exists(keyz);
assert.isArray(keyz);
assert.lengthOf(keyz, 3);
});
it("should have keys n1 n2 n3", async function() {
const keyz = await model.keylist();
assert.exists(keyz);
assert.isArray(keyz);
assert.lengthOf(keyz, 3);
for (let key of keyz) {
assert.match(key, /n[123]/, "correct key");
}
});
it("should have titles Node #", async function() {
const keyz = await model.keylist();
assert.exists(keyz);
assert.isArray(keyz);
assert.lengthOf(keyz, 3);
var keyPromises = keyz.map(key => model.read(key));
const notez = await Promise.all(keyPromises);
for (let note of notez) {
assert.match(note.title, /Note [123]/, "correct title");
}
});
});
The idea is to call Notes API functions, then to test the results to check whether they matched the expected results.
This describe block is within the outer describe block. The descriptions given in the describe and it blocks are used to make the test report more readable. The it block forms a pseudo-sentence along the lines of it (the thing being tested) should do this or that.
Even though Mocha requires regular functions for the describe and it blocks, we can use arrow functions within those functions.
How does Mocha know whether the test code passes? How does it know when the test finishes? This segment of code shows one of the three methods.
Generally, Mocha is looking to see if the function throws an exception, or whether the test case takes too long to execute (a timeout situation). In either case, Mocha will indicate a test failure. That's of course simple to determine for non-asynchronous code. But, Node.js is all about asynchronous code, and Mocha has two models for testing asynchronous code.
In the first (not seen here), Mocha passes in a callback function, and the test code is to call the callback function. In the second, as seen here, it looks for a Promise being returned by the test function, and determines pass/fail on whether the Promise is in the resolve or reject state.
In this case, we're using async functions, because they automatically return a Promise. Within the functions, we're calling asynchronous functions using await, ensuring any thrown exception is indicated as a rejected Promise.
Another item to note is the question asked earlier: what if the callback function we're testing is never called? Or, what if a Promise is never resolved? Mocha starts a timer and if the test case does not finish before the timer expires, Mocha fails the test case.