Functional Testing with Mocha & Chai
Learn how to write automated JavaScript tests with Mocha and Chai on an MBaaS.
Join the DZone community and get the full member experience.
Join For FreeIn full stack JavaScript development involving JavaScript on either or both the client-side and the server-side, we would like to test function input-output behavior. In its simplest form, function input-output behavior specifies the output of the function in terms of its input.
For example, for a function adding two numbers:
function f(x,y){
return x + y;
}
Its input-output behavior specifies abstractly that,
f(x,y) == x + y
Functions can executed either locally, or involve calls to remote services. A local execution of a function is the classical in-program execution that does not involve any server-side calls.
Such functions can be JavaScript functions, Node.js functions, and can be embedded in frameworks such as AngularJS.
Automated Testing
Testing abstract assertions such as the one above is not practical for most cases. Instead, automated testing attempts to automatically run test scripts that check correctness of important cases. If employed judiciously, automated testing can achieve several goals:
- It documents the tests used in manual testing while coding
- Regression testing
- Continuous integration based on facts not intuition
Automated testing requires a test runner, a program that understands test scripts written in the language of the test runner. A test script includes several function invocations, and some assertions on the input-output behavior of the functions, and automated checking of assertions expressed in an assertion language.
Mocha and Chai
We have found Mocha as a test runner, and Chai as an assertion language a powerful combination for full stack automated testing. In contrast to other test runners, such as Jasmine, Mocha is not tied to any particular assertion language. Chai itself comes with several flavours of BDD and TDD assertions. We found Chai’s BDD expect style to be very convenient for both local function execution, and for execution involving calls to remote services.
So how does a Mocha test script look?
Mocha Test
A mocha test is built ‘describe’ statements that can be nested. Each describe statement is a test scenario. To actually perform a test, use an ‘it’ statement:
describe("function f", function(){
it("adds numbers", function(done){
expect(f(3, 4)).to.be.equal(7);
done();
});
});
The Chai assertion using ‘expect’ is used to make assertions on the input-output behavior of the function.
The ‘done’ callback is used to transfer to the next test.
Using Mocha with Chai in Backand
We have used Mocha with Chai in Backand, a Backend as a Service (BaaS) provider, that automatically creates a relational database together with a REST API from a JSON schema describing the database in familiar JavaScript terms. It was inspired by the Waterline ORM.
For example, this schema JSON:
[{ "name": "R",
"fields": {
C: { type: “integer” },
D: { type: “string”, required: true }
}
},
{ "name": "U",
"fields": {
E: { type: “integer” },
F: { type: “string”, required: true },
H: { type: “string” }
}
}]
Creates a database with two relations:
CREATE TABLE `P` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`C` int(11) DEFAULT NULL,
`D` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `Q` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`E` int(11) DEFAULT NULL,
`F` varchar(255) NOT NULL,
`H` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB DEFAULT CHARSET=latin1;
Using these ‘create table’ statements in MySQL:
create table `S` (`id` int unsigned not null auto_increment primary key, `C` int, `D` varchar(255) not null);
create table `U` (`id` int unsigned not null auto_increment primary key, `E` int, `F` varchar(255) not null, `H` varchar(255));
Our Testing Needs
We had two major functions, ‘validate’ that tests whether a JSON is a valid relational schema, and ‘transform’ that transform one schema to another. Backand uses ‘transform’, when Backand users modify their database structure by providing a new JSON schema. The function create a sequence of MySQL statements to modify the underlying MySQL database.
While coding, we have accumulated a collection of test cases, and we found Mocha with Chair useful for documenting them. This was done immediately during coding. Having completed the first phase of coding, we expanded the use of automated testing to regression testing. We continuously expand the expressive power of the schema, for example into one-to-many relationships between relations in the database. Automated testing enabled convenient regression testing.
Writing Test
To install Mocha and Chai, we used ‘package.json':
{
"name": "test",
"version": "0.0.0",
"description": "test orm",
"author": "Yoram Kornatzky",
"dependencies": {
"mocha": "latest",
"chai": "latest"
}
}
Installing them with:
npm install
In our test file, ‘unit_functional_test.js’, we include Chai, and then our code:
var expect = require("chai").expect;
var validator = require("../validate_schema.js").validator;
var transformer = require("../transform.js").transformer;
Now we write a test to check that our validator validates as valid a valid schema.
describe("validator has no false negatives", function(){
it("declare valid schema with default values and non null columns", function(done){
var v = validator([{ "name": "R",
"fields": {
"A": { "type": "float", "defaultValue": 20 },
"B": { "type": "string", "required": true }
}
},
{ "name": "U",
"fields": {
"F": { "type": "string", "required": true },
"G": { "type": "float" },
"H": { "type": "string" }
}
}]);
expect(v).to.deep.equal({ valid: true, warnings: [] });
done();
});
});
We run the test with:
node_modules/mocha/bin/mocha unit_functional_test.js
And get this output:
Another test to validate that default values of fields are correctly validated is:
it("default values of columns are of the right type", function(done){
var v = validator(
[
{ name: "user",
fields: {
name: { type: 'string', defaultValue: 200 },
age: { type: 'datetime', defaultValue: '2015-09-08' },
dogs:{ collection: 'pet', via: 'owner' }
}
},
{ name: "pet",
fields: {
name: { type: 'string' },
registered: { type: 'boolean' },
owner:{ object: 'user' }
}
}
]
);
expect(v).to.deep.equal({ valid: false,
warnings: ["column default value should be a string:user name"]});
done();
});
Which in this case should detect the the ‘name’ column in the table ‘user’ has a non valid default value.
To test that the SQL statements to transform one database schema to another are correct, we use this test:
it("drop columns", function(done){
var r = transformer(
[
{ name: "R",
fields: {
a: { type: "float" },
b: { type: "string" },
dogs: { collection: "U", via: "owner" }
}
},
{ name: "U",
fields: {
c: { type: "float" },
d: { type: "string" },
owner: { object: 'R' }
}
}
],
[
{ name: "R",
fields: {
b: { type: "string" }
}
},
{ name: "U",
fields: {
c: { type: "float" },
d: { type: "string" }
}
}
]
);
expect(r).to.deep.equal(
{
"alter": [
"alter table `R` drop `a`",
"alter table `U` drop `owner`"
]
});
Testing Remote Service Calls
In full stack development, almost any module, be it on the client side or the server side, involves remote service calls. Such calls, while being asynchronous, are actually function invocations that yield an output for input. Mocha and Chai are just as convenient for testing such calls.
In Backand, we fetch the details of the MySQL database when you supply your username, password, and app name. This is described in the following test, where we use the ‘request’ package to make HTTP calls.
First, we require our module making remote service calls:
var connectionInfo = require("../get_connection_info");
Then we make a call to get the authentication token for an authorised user and app. Once we get the token, we call the getConnectionInfo service. This test is intended to test the input-output behaviour of the getConnectionInfo service. So the assertions are on the output of that service.
describe("get connection info", function(){
it("get connection info with correct credentials", function(done){
this.timeout(4000);
// credentials
var email = "johndoe@example.com";
var password = "secret";
var appName = "myApp";
// request authentication token
request(
{
url: tokenUrl,
method: 'POST',
form: {
username: email,
password: password,
appName: appName
}
},
function(error, response, body){
if (!error && response.statusCode == 200) {
var b = JSON.parse(body)
var accessToken = b["access_token"];
var tokenType = b["token_type"];
// now call the remote service
connectionInfo.getConnectionInfo(accessToken,
tokenType, appName,
function(err, result){
expect(result).to.deep.equal(
{
hostname: 'cd2junihlkms.aws.com',
port: '3306',
db: 'backandmyapp',
username: 'lmezyl4j',
password: ‘our_db_secret’
}
);
done();
});
}
else{
done();
}
}
);
});
});
We increase the standard Mocha timeout of 2000ms to 4000ms because a call to the server may take time.
Published at DZone with permission of Itay Herskovits, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments