├── Makefile
├── README.md
├── img
├── nyan-fail.png
└── nyan-win.png
├── package.json
├── server.js
└── test
└── test.js
/Makefile:
--------------------------------------------------------------------------------
1 | TESTS = test/*.js
2 | test:
3 | mocha --timeout 5000 --reporter nyan $(TESTS)
4 |
5 | .PHONY: test
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # API Testing with Node.js
2 |
3 | ---
4 |
5 | > Testing an API with Node.js is dead simple. You can quickly write tests for any API, regardless of its language.
6 |
7 | ### Hackers Don't Test
8 | I don't like writing tests. Just ask any of my coding teammates. I spent too many years slapping together quick protoypes that were just for proof of concept and didn't need to be maintained. I think of testing like financial accounting; I know it's vital to keep operations running smoothly, but I much prefer if somebody else handles it.
9 |
10 | I'm getting better though, and it's paying off. You've probably already read about the reasons you should write tests. If you're still not convinced, consider this: it's quickly becoming table stakes for most modern web development projects, much like knowing how to use [Github](http://github.com). If you want to contribute to open source projects, or make it past your first interview, you've gotta write tests.
11 |
12 | ### Unit, Integration, Functional, and Acceptance
13 | I'm going to skip the [philosophy](http://www.agitar.com/downloads/TheWayOfTestivus.pdf) and finer points. Read this answer for a great explanation of the [different types of tests](http://stackoverflow.com/a/4904533). On the most granular level we have unit tests. On the other end of the spectrum we have browser-testing tools like [PhantomJS](http://phantomjs.org/) or SaaS options like [BrowserStack](http://browserstack.com) and [Browserling](http://browserling.com). We're going to be closer to that high level testing, but since this is purely an API, we don't need a browser.
14 |
15 | ### Our Example API
16 | Let's take a look at our example API, which is a protected pretend blog. In order to show off some of the testing options, this API:
17 |
18 | - requires Basic Auth
19 | - requires an API key be passed in as a custom header
20 | - always returns JSON
21 | - sends proper status codes with errors
22 |
23 | ### Mocha, Chai, and SuperTest
24 | If you've spent 30 minutes tinkering with Node.js, there's a really good chance you've seen the work of [TJ Holowaychuck](https://github.com/visionmedia) and his army of ferrets. His [Mocha testing framework](http://mochajs.org/) is popular and we'll be using it as a base. Fewer people have seen his [SuperTest](https://github.com/visionmedia/supertest) library, which adds some really nice shortcuts specifically for testing HTTP calls. Finally, we're including [Chai](http://chaijs.com/) just to round out the syntax goodness.
25 |
26 | var should = require('chai').should(),
27 | supertest = require('supertest'),
28 | api = supertest('http://localhost:5000');
29 |
30 | Note that we're passing in the base URL of our API. As a sidenote, if you're writing your API in [Express](http://expressjs.com), you can use SuperTest to hook write into your application without actually running it as a server.
31 |
32 | ### Test Already!
33 | Install Mocha (`npm install -g mocha`) and check out the [getting started section](http://mochajs.org/#getting-started). To summarize, you can group little tests (assertions) within `it()` functions, and then group those into higher level groups within `describe` functions. How many things you test in each `it` and how you group those into `describe` blocks is mostly a matter of style and preference. You'll also evenutally end up using the `before` and `beforeEach` features, but our sample test doesn't need them.
34 |
35 | Let's start with authentication. We want to make sure this API returns proper errors if somebody doesn't get past our two authentication checks:
36 |
37 | [See the gist](https://gist.github.com/jedwood/5311084)
38 |
39 |
40 | Don't let the syntax on lines 3 and 10 throw you- we're just giving the tests names that will make sense to us when we view our reports. You can put pretty much anything in there. In both tests (the `it` calls), we make a `get` call to `/blog`.
41 |
42 | In our first test, we use `set` to add our custom header with a correct value, but then we set put some bad credentials in `auth` (which creates the BasicAuth header). We're expecting a proper `401` status.
43 |
44 | In our second test, we set the correct BasicAuth credentials but intentionally omit the `x-api-key`. We're again expecting a `401` status.
45 |
46 | ### Run Nyan Run!
47 | We're almost ready to run these tests. Let's follow TJ's advice:
48 |
49 | > Be kind and don't make developers hunt around
50 | > in your docs to figure out how to run the tests,
51 | > add a `make test` target to your `Makefile`
52 |
53 | So here's what that looks like:
54 |
55 | TESTS = test/*.js
56 | test:
57 | mocha --timeout 5000 --reporter nyan $(TESTS)
58 |
59 | .PHONY: test
60 |
61 | *[UPDATE TWO YEARS LATER]* It's now pretty common to instead include your test command as part of the `scripts` block in the `package.json` file. Then tests can be run with `npm test`.
62 |
63 | The two flags in there are increasing the default timeout of 2000ms and telling Mocha to use the excellent [nyan reporter](https://vimeo.com/44180900).
64 |
65 | Finally, we jump over to our terminal and run `make test`. Here's what we get:
66 |
67 | 
68 |
69 | Uh oh. Looks like the API is returning a message that looks like an error, but the status is `200`. Let's fix that in the API code by changing this:
70 |
71 | `res.send({error: "Bad or missing app identification header"});`
72 |
73 | to this:
74 |
75 | `res.status(401).send({error: "Bad or missing app identification header"});`
76 |
77 | ### Expect More
78 | Like all good developers, I'm going to be confident that I've fixed it and move on before re-running the report. Let's add another test:
79 |
80 | [See the gist](https://gist.github.com/jedwood/5311429)
81 |
82 |
83 | This one has an "end" callback because we're going to be inspecting the actual results of the response body. We send correct authentication and then check four aspects of the result:
84 |
85 | - line 7 checks for 200 status
86 | - line 8 makes sure the format is JSON
87 | - line 11 is a chain that checks for a `posts` property, and also makes sure it's an `array`.
88 |
89 | It's Chai that's giving us that handy.way.of.checking.things.
90 |
91 | Run the tests again...wait 46ms...and...
92 |
93 | 
94 |
95 | Happy Nyan!
96 |
97 | ### Hackers Don't Mock
98 | Another common component of testing is the notion of using "mock" objects and/or a sandboxed testing database. What I like about the setup we've covered here is that it doesn't care. We can run our target server in production or dev or testing mode depending on our purpose and risk aversion without changing our tests. Finer-grained unit testing and mock objects certainly have their place, but a lot can go wrong in between those small abstracted pieces and your full production environment. High-level acceptance tests like the ones we've built here can broadly cover the end user touchpoints. If an error crops up, you'll know where to start digging.
99 |
100 | _Now go test all the things!_
101 |
--------------------------------------------------------------------------------
/img/nyan-fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jedwood/api-testing-with-node/9161eb735069f3cbd27a40fcacf4771daf061ce7/img/nyan-fail.png
--------------------------------------------------------------------------------
/img/nyan-win.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jedwood/api-testing-with-node/9161eb735069f3cbd27a40fcacf4771daf061ce7/img/nyan-win.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-testing",
3 | "version": "0.0.1",
4 | "engines": {
5 | "node": "0.8.x",
6 | "npm": "1.1.x"
7 | },
8 | "private": true,
9 | "scripts": {
10 | "start": "node server.js"
11 | },
12 | "dependencies": {
13 | "express": "3.1.0"
14 | },
15 | "description": "",
16 | "main": "server.js",
17 | "devDependencies": {
18 | "mocha": "*",
19 | "chai": "*",
20 | "supertest": "*"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": ""
25 | },
26 | "author": "jedwood"
27 | }
28 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express'),
2 | app = express();
3 |
4 | app.configure(function(){
5 | app.set('port', process.env.PORT || 5000);
6 | app.use(express.bodyParser());
7 | app.use(express.methodOverride());
8 | app.use(app.router);
9 | });
10 |
11 | app.configure('development', function(){
12 | app.use(express.logger('dev'));
13 | app.use(express.errorHandler());
14 | });
15 |
16 | app.all('/*', function(req, res, next) {
17 | //IRL, lookup in a database or something
18 | if (typeof req.headers['x-api-key'] !== 'undefined' && req.headers['x-api-key'] === '123myapikey') {
19 | next();
20 | } else {
21 | res.status(401).send({error: "Bad or missing app identification header"});
22 | }
23 | });
24 |
25 | app.get('/blog', express.basicAuth('correct', 'credentials'), function(req, res) {
26 | res.send({posts:['one post', 'two post']});
27 | });
28 |
29 | app.listen(app.get('port'), function(){
30 | console.log("Example API listening on port " + app.get('port') + ', running in ' + app.settings.env + " mode.");
31 | });
32 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var should = require('chai').should(),
2 | supertest = require('supertest'),
3 | api = supertest('http://localhost:5000');
4 |
5 | describe('Authentication', function() {
6 |
7 | it('errors if wrong basic auth', function(done) {
8 | api.get('/blog')
9 | .set('x-api-key', '123myapikey')
10 | .auth('incorrect', 'credentials')
11 | .expect(401, done)
12 | });
13 |
14 | it('errors if bad x-api-key header', function(done) {
15 | api.get('/blog')
16 | .auth('correct', 'credentials')
17 | .expect(401)
18 | .expect({error:"Bad or missing app identification header"}, done);
19 | });
20 |
21 | });
22 |
23 |
24 | describe('/blog', function() {
25 |
26 | it('returns blog posts as JSON', function(done) {
27 | api.get('/blog')
28 | .set('x-api-key', '123myapikey')
29 | .auth('correct', 'credentials')
30 | .expect(200)
31 | .expect('Content-Type', /json/)
32 | .end(function(err, res) {
33 | if (err) return done(err);
34 | res.body.should.have.property('posts').and.be.instanceof(Array);
35 | done();
36 | });
37 | });
38 |
39 | });
40 |
--------------------------------------------------------------------------------