├── .gitignore ├── README.md ├── lib ├── Server.js └── User.js ├── main.js ├── package.json └── test ├── integration └── Users.js └── unit ├── Math.js └── User.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Writing testable HTTP APIs using Node.js - the basics 2 | 3 | I remember writing my first test cases - it was everything but nice and clean. 4 | Testing done right is not easy. 5 | 6 | It is not just about how you write your tests, but also how you design your entire codebase. 7 | This post intends to give an insight on how we develop testable HTTP APIs at [RisingStack](http://risingstack.com). 8 | 9 | All the examples use [Joyent's](http://www.joyent.com/) [restify framework](https://github.com/mcavage/node-restify), 10 | and can be found at [RisingStack's Github](https://github.com/RisingStack/writing-testable-apis-the-basics) page. 11 | 12 | ## Setting up your test environment 13 | 14 | In order to run our test cases, we need a test runner and an assertion library. 15 | The test runner will sequentially run each test case, while the assertion library 16 | will check if the expected value equals the outcome. 17 | 18 | But enough of the theory, let's setup our test runner and assertion library! 19 | For this post, we will use mocha as our test runner, and chai as our assertion library. 20 | 21 | ### Adding mocha to your project 22 | 23 | ``` 24 | npm install mocha --save-dev 25 | mkdir test 26 | ``` 27 | 28 | It will install [mocha](http://mochajs.org/) and put it as a development 29 | dependency to your `package.json`. Then you should put all your test cases 30 | under the `test` folder. 31 | 32 | Also, it is convenient to put it into your `package.json`'s scripts section, so it can be 33 | run using the `npm test` command. 34 | 35 | ```javascript 36 | "scripts": { 37 | "test": "mocha test" 38 | } 39 | ``` 40 | 41 | This will work without installing mocha globally, as npm will look for `node_modules/.bin`, and 42 | place it on the `PATH`. 43 | 44 | ### Adding chai to your project 45 | 46 | ```bash 47 | npm install chai --save-dev 48 | ``` 49 | 50 | Then using [chai](http://chai.com) it is time to write the first test case, just to demonstrate 51 | how mocha and chai plays together. 52 | 53 | ```javascript 54 | // test/string.js 55 | var expect = require('chai').expect; 56 | 57 | describe('Math', function () { 58 | describe('#max', function () { 59 | it('returns the biggest number from the arguments', function () { 60 | var max = Math.max(1, 2, 10, 3); 61 | expect(max).to.equal(10); 62 | }); 63 | }); 64 | }); 65 | ``` 66 | 67 | The above test can be run with `npm test`. 68 | 69 | ## Designing your codebase - time for unit tests 70 | 71 | Unit tests are the basic building block of tests, where each test case is independent from 72 | others. Unit tests provide a living documentation of the system and are 73 | extremely valuable for design feedback: one looking at your test cases can figure out easily what 74 | the given unit does, how you engineered it, what interfaces does it expose. 75 | 76 | As a side effect, unit tests can verify if your units work correctly. 77 | 78 | The magic word here is: TDD, meaning Test-driven development. 79 | TDD is the process of writing an initially failing test case, that defines 80 | a function - this is where you design the interfaces of your unit. 81 | 82 | After that all you have to do is make the tests pass by implementing the described 83 | functionality. 84 | 85 | When writing unit tests, we do not want to deal with the given unit's dependencies, 86 | so we want to use mocks instead of them. Mocks are special objects that simulate the 87 | behavior of the mocked out dependencies. For this purpose we are going to use 88 | [Sinon.JS](http://sinonjs.org). 89 | 90 | Let's take an example of mocking out MongoDB. Sure, first you will need `sinon` installed. 91 | 92 | ```bash 93 | npm install sinon --save-dev 94 | ``` 95 | 96 | As we are doing TDD, first let's write our (initially) failing unit test for a Mongoose model. 97 | It will be a model called `User` with a static method `findUnicorns`. 98 | 99 | How to do this? Let's take a look at the necessary steps: 100 | 101 | - start with the test setup 102 | - calling the object under test's method 103 | - finally asserting 104 | 105 | ```javascript 106 | var sinon = require('sinon'); 107 | var expect = require('chai').expect; 108 | 109 | var mongoose = require('mongoose'); 110 | var User = require('./../../lib/User'); 111 | var UserModel = mongoose.model('User'); 112 | 113 | describe('User', function() { 114 | it('#findUnicorns', function(done) { 115 | 116 | // test setup 117 | var unicorns = [ 'unicorn1', 'unicorn2' ]; 118 | var query = { world: '1' }; 119 | 120 | // mocking MongoDB 121 | sinon.stub(UserModel, 'findUnicorns').yields(null, unicorns); 122 | 123 | // calling the test case 124 | User.colorizeUnicorns(query, function(err, coloredUnicorns) { 125 | 126 | // asserting 127 | expect(err).to.be.null; 128 | expect(coloredUnicorns).to.eql(['unicorn1-pink', 'unicorn2-purple']); 129 | 130 | // as our test is asynchronous, we have to tell mocha that it is finished 131 | done(); 132 | }); 133 | }); 134 | }); 135 | ``` 136 | 137 | Nice, huh? The "only" job left here is to do the actual implementation. 138 | (it can be found in the `lib` folder) 139 | 140 | ## Putting the pieces together - writing integration tests 141 | 142 | All our unit tests are passing, great! But how will the system as a whole function? 143 | This is where integration tests come in. 144 | 145 | During integration testing unit tested parts are combined to verify functional and 146 | performance requirements. 147 | 148 | For integration tests we will use [hippie](https://github.com/vesln/hippie). 149 | hippie is a thin request wrapper that enables powerful and intuitive API testing. 150 | 151 | Add hippie to your project with: 152 | 153 | ```bash 154 | npm install hippie --save-dev 155 | ``` 156 | 157 | To demonstrate what hippie is capable of, let's create an HTTP endpoint! 158 | 159 | This endpoint will serve `GET` requests at `/users`. For building APIs using JSON, 160 | you can use [json:api](http://jsonapi.org/) as a reference. 161 | 162 | ```javascript 163 | var hippie = require('hippie'); 164 | var server = require('../../lib/Server'); 165 | 166 | describe('Server', function () { 167 | describe('/users endpoint', function () { 168 | it('returns a user based on the id', function (done) { 169 | hippie(server) 170 | .json() 171 | .get('/users/1') 172 | .expectStatus(200) 173 | .end(done); 174 | }); 175 | }); 176 | }); 177 | ``` 178 | 179 | The above example queries the server for the user with id=1. You can check the 180 | basic implementation in the `lib/Server.js` file. 181 | 182 | ## Next up 183 | 184 | Now you have learnt all the basics - but what will come next? We will dive deeper in how we 185 | write APIs at [RisingStack](http://risingstack.com), including mocking external APIs like Facebook and 186 | debug performances issues using DTrace. 187 | -------------------------------------------------------------------------------- /lib/Server.js: -------------------------------------------------------------------------------- 1 | var restify = require('restify'); 2 | var server = restify.createServer(); 3 | 4 | server.get('/users/:id', function(req, res){ 5 | res.json({ 6 | id: 1, 7 | name: 'RisingStack' 8 | }); 9 | }); 10 | 11 | module.exports = server; 12 | -------------------------------------------------------------------------------- /lib/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | 7 | /** 8 | * User schema. 9 | */ 10 | 11 | var schema = new mongoose.Schema({ 12 | name: String, 13 | world: String 14 | }); 15 | 16 | /** 17 | * Find all unicorns in given `world`. 18 | * 19 | * @param {String} world 20 | * @param {Function} fn 21 | * @api public 22 | * @static 23 | */ 24 | 25 | schema.statics.findUnicorns = function(world, fn) { 26 | this.find({ world: world }, fn); 27 | }; 28 | 29 | /** 30 | * Register the model. 31 | */ 32 | 33 | var User = mongoose.model('User', schema); 34 | 35 | /** 36 | * Colorize unicorns 37 | * 38 | * @param {Object} query 39 | * @param {Function} fn 40 | */ 41 | 42 | exports.colorizeUnicorns = function(query, fn) { 43 | User.findUnicorns(query.world, function (err, unicorns) { 44 | if(err) { 45 | return fn(err); 46 | } 47 | 48 | unicorns = unicorns.map(function (uni, key) { 49 | uni += key % 2 ? '-purple' : '-pink'; 50 | return uni; 51 | }); 52 | 53 | return fn(null, unicorns); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var app = require('./lib/Server'); 5 | 6 | http.createServer(app).listen(3000, function (err) { 7 | if (err) { 8 | return console.log(err); 9 | } 10 | console.log('Server is running at port 3000'); 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "risingstack-blogpost", 3 | "version": "0.0.0", 4 | "description": "Writing testable HTTP APIs - the basics", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test --recursive" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/oroce/risingstack-blogpost.git" 12 | }, 13 | "author": "Gergely Nemeth (http://twitter.com/nthgergo)", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/oroce/risingstack-blogpost/issues" 17 | }, 18 | "homepage": "https://github.com/oroce/risingstack-blogpost", 19 | "dependencies": { 20 | "mongoose": "^3.8.12", 21 | "restify": "^2.8.1" 22 | }, 23 | "devDependencies": { 24 | "chai": "^1.9.1", 25 | "hippie": "^0.3.0", 26 | "mocha": "^1.20.1", 27 | "sinon": "^1.10.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/integration/Users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hippie = require('hippie'); 4 | 5 | var server = require('../../lib/Server'); 6 | 7 | describe('Server', function () { 8 | 9 | describe('/users endpoint', function () { 10 | 11 | it('returns a user based on the id', function (done) { 12 | hippie(server) 13 | .json() 14 | .get('/users/1') 15 | .expectStatus(200) 16 | .end(function(err, res, body) { 17 | if (err) throw err; 18 | done(); 19 | }); 20 | }); 21 | 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/Math.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | describe('Math', function () { 4 | 5 | describe('#max', function () { 6 | 7 | it('returns the biggest number from the arguments', function () { 8 | var max = Math.max(1, 2, 10, 3); 9 | expect(max).to.equal(10); 10 | }); 11 | 12 | }); 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /test/unit/User.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var expect = require('chai').expect; 3 | 4 | var mongoose = require('mongoose'); 5 | var User = require('./../../lib/User'); 6 | var UserModel = mongoose.model('User'); 7 | 8 | describe('User', function() { 9 | it('#findUnicorns', function(done) { 10 | 11 | // test setup 12 | var unicorns = [ 'unicorn1', 'unicorn2' ]; 13 | var query = { world: '1' }; 14 | 15 | // mocking MongoDB 16 | sinon.stub(UserModel, 'findUnicorns').yields(null, unicorns); 17 | 18 | // calling the test case 19 | User.colorizeUnicorns(query, function(err, coloredUnicorns) { 20 | 21 | // asserting 22 | expect(err).to.be.null; 23 | expect(coloredUnicorns).to.eql(['unicorn1-pink', 'unicorn2-purple']); 24 | 25 | // as our test is asynchronous, we have to tell mocha that it is finished 26 | done(); 27 | }); 28 | }); 29 | }); 30 | --------------------------------------------------------------------------------