Writing Testable HTTP APIs Using Node.js - The Basics
I remember writing my first test cases - it was everything but nice and clean.Testing done right is not easy.
It is not just about how you write your tests, but also how you design your entire codebase.This post intends to give an insight on how we develop testable HTTP APIs at RisingStack.
All the examples use Joyent's restify framework,and can be found at RisingStack's Github page.
Setting up your test environment
In order to run our test cases, we need a test runner and an assertion library.The test runner will sequentially run each test case, while the assertion librarywill check if the expected value equals the outcome.
But enough of the theory, let's setup our test runner and assertion library!For this post, we will use mocha as our test runner, and chai as our assertion library.
Adding mocha to your project
npm install mocha --save-devmkdir test
It will install mocha and put it as a developmentdependency to your package.json
. Then you should put all your test casesunder the test
folder.
Also, it is convenient to put it into your package.json
's scripts section, so it can berun using the npm test
command.
"scripts": { "test": "mocha test" }
This will work without installing mocha globally, as npm will look for node_modules/.bin
, andplace it on the PATH
.
Adding chai to your project
npm install chai --save-dev
Then using chai it is time to write the first test case, just to demonstratehow mocha and chai plays together.
// test/string.jsvar expect = require('chai').expect;describe('Math', function () { describe('#max', function () { it('returns the biggest number from the arguments', function () { var max = Math.max(1, 2, 10, 3); expect(max).to.equal(10); }); });});
The above test can be run with npm test
.
Designing your codebase - time for unit tests
Unit tests are the basic building block of tests, where each test case is independent fromothers. Unit tests provide a living documentation of the system and areextremely valuable for design feedback: one looking at your test cases can figure out easily whatthe given unit does, how you engineered it, what interfaces does it expose.
As a side effect, unit tests can verify if your units work correctly.
The magic word here is: TDD, meaning Test-drive development.TDD is the process of writing an initially failing test case, that definesa function - this is where you design the interfaces of your unit.
After that all you have to do is make the tests pass by implementing the describedfunctionality.
When writing unit tests, we do not want to deal with the given unit's dependencies,so we want to use mocks instead of them. Mocks are special objects that simulate thebehavior of the mocked out dependencies. For this purpose we are going to useSinon.JS.
Let's take an example of mocking out MongoDB. Sure, first you will need sinon
installed.
npm install sinon --save-dev
As we are doing TDD, first let's write our (initially) failing unit test for a Mongoose model.It will be a model called User
with a static method findUnicorns
.
How to do this? Let's take a look at the necessary steps:
- start with the test setup
- calling the object under test's method
- finally asserting
var sinon = require('sinon');var expect = require('chai').expect;var mongoose = require('mongoose');var User = require('./../../lib/User');var UserModel = mongoose.model('User');describe('User', function() { it('#findUnicorns', function(done) { // test setup var unicorns = [ 'unicorn1', 'unicorn2' ]; var query = { world: '1' }; // mocking MongoDB sinon.stub(UserModel, 'findUnicorns').yields(null, unicorns); // calling the test case User.colorizeUnicorns(query, function(err, coloredUnicorns) { // asserting expect(err).to.be.null; expect(coloredUnicorns).to.eql(['unicorn1-pink', 'unicorn2-purple']); // as our test is asynchronous, we have to tell mocha that it is finished done(); }); });});
Nice, huh? The "only" job left here is to do the actual implementation.(it can be found in the lib
folder)
Putting the pieces together - writing integration tests
All our unit tests are passing, great! But how will the system as a whole function?This is where integration tests come in.
During integration testing unit tested parts are combined to verify functional andperformance requirements.
For integration tests we will use hippie.hippie is a thin request wrapper that enables powerful and intuitive API testing.
Add hippie to your project with:
npm install hippie --save-dev
To demonstrate what hippie is capable of, let's create an HTTP endpoint!
This endpoint will serve GET
requests at /users
. For building APIs using JSON,you can use json:api as a reference.
var hippie = require('hippie');var server = require('../../lib/Server');describe('Server', function () { describe('/users endpoint', function () { it('returns a user based on the id', function (done) { hippie(server) .json() .get('/users/1') .expectStatus(200) .end(function(err, res, body) { if (err) throw err; done(); }); }); });});
The above example queries the server for the user with id=1. You can check thebasic implementation in the lib/Server.js
file.
Next up
Now you have learnt all the basics - but what will come next? We will dive deeper in how wewrite APIs at RisingStack, including mocking external APIs like Facebook anddebug performances issues using DTrace.
Post written by Gergely Nemeth, RisingStack