Testing Express.js + CouchDB Applications with mock-couch
Posted by Justas Ažna in archive
Prerequisites
Originally posted on 2015-07-05 at fadeit.dk/blog which is no longer available
The code in this post is based upon CoffeeScript, Mocha and Gulp but it should be easily adaptable to other stacks (e.g. JavaScript, Jasmine and Grunt).
Intro
Recently, I worked on a Express.js based RESTful API with CouchDB storage. For various reasons, I mostly had end-to-end tests (testing HTTP endpoints) and very few unit tests. In the tests, I would rebuild the database before every describe
block. This worked quite well but at one point I noticed that my feedback loops were beginning to get tediously slow (~30 seconds to run the full test suite). Thankfully, I discovered mock-couch: in-memory storage with CouchDB API. The switch was rather easy and, as a result, my test suite would finish in less than 10 seconds (even when reseting test data before each test).
Setup
Our sample API will expose functionality to access a database of sofa’s. Let’s set it up.
Structure and dependencies
mkdir -p sofa-api/specs
cd sofa-api
npm install mock-couch coffee-script gulp gulp-mocha should supertest express nano q
gulpfile.coffee
require 'coffee-script/register'
gulp = require 'gulp'
mocha = require 'gulp-mocha'
sources =
tests: './specs/**/*.spec.coffee'
# Task to run tests with mock-couch backend
gulp.task 'test', ->
process.env.NODE_ENV = 'test'
process.env.PORT = 3010
process.env.COUCH_PORT = 5987
process.env.DATABASE = 'sofas_test'
gulp.src(sources.tests)
.pipe(mocha())
# Task to run integration tests (real CouchDB backend)
gulp.task 'test-int', ->
process.env.NODE_ENV = 'test-int'
process.env.PORT = 3010
process.env.COUCH_PORT = 5984
process.env.DATABASE = 'sofas_test'
gulp.src(sources.tests)
.pipe(mocha())
# Task to run our server
gulp.task 'default', ->
process.env.NODE_ENV = 'development'
process.env.PORT = 3000
process.env.COUCH_PORT = 5984
process.env.DATABASE = 'sofas'
server = require './server'
server.listen()
server.coffee
express = require 'express'
app = express()
app.get '/sofas', (req, res) ->
throw new Error 'not implemented'
app.get '/sofas/:sofa', (req, res) ->
throw new Error 'not implemented'
exports.listen = (callback) ->
port = process.env.PORT
app.listen port, callback
Let’s create file specs/sofas.spec.coffee which is going to contain our tests.
should = require 'should'
describe 'Sofas API', ->
describe '/sofas', ->
it 'should return all sofas with GET', ->
# TODO: write test
(false).should.be.true()
describe '/sofas/:sofa', ->
it 'should return a particular sofa with GET', ->
# TODO: write test
(false).should.be.true()
If you run the tests right now, you should see similar output:
gulp test
Sofas API
/sofas
1) should return all sofas with GET
/sofas/:sofa
2) should return a particular sofa with GET
0 passing (30ms)
2 failing
1) Sofas API /sofas should return all sofas with GET:
AssertionError: expected false to be true
at Context.<anonymous> (specs/sofas.spec.coffee:8:7)
2) Sofas API /sofas/:sofa should return a particular sofa with GET:
AssertionError: expected false to be true
at Context.<anonymous> (specs/sofas.spec.coffee:13:7)
Message:
2 tests failed.
Implementation
Sofas will be stored in CouchDB with the following structure:
{
"_id": "CouchDB assigned id",
"_rev": "CouchDB assigned revision",
"name": "Name of the sofa",
"price": "Price of the sofa"
}
With this in mind, let’s build a module for accessing the database.
nano = require 'nano'
Q = require 'q'
class DAL
constructor: (port, @database) ->
@conn = nano("http://localhost:#{port}")
# Returns a promise reseting database
resetDatabase: (withSamples=false) ->
docs = [@_sofaViews]
if withSamples
docs = docs.concat @_sofaSamples
@_deleteDb()
.then => @_createDb()
.then => Q.all(docs.map (d) => @_insertDoc(d))
# Returns a promise of sofas
# If you provide the id- you'll get an array with a particular sofa in it
getSofas: (id=null) ->
params = {}
params.key = id if id?
@_view('sofas', 'all', params)
.then (res) ->
body = res[0]
sofas = (row.value for row in body.rows)
return sofas
_sofaViews:
_id: '_design/sofas'
views:
all:
map: (doc) ->
if doc.type is 'Sofa'
emit doc._id, doc
_sofaSamples: [
{
_id: 'sofa1'
type: 'Sofa'
name: 'Sofa1'
price: 400
}
{
_id: 'sofa2'
type: 'Sofa'
name: 'Sofa2'
price: 300
}
{
_id: 'sofa3'
type: 'Sofa'
name: 'Sofa3'
price: 500
}
]
# Returns promise of creating database
_createDb: ->
d = Q.defer()
@conn.db.create @database, d.makeNodeResolver()
d.promise
# Returns promise of deleting database
_deleteDb: (ignoreMissing=true)->
d = Q.defer()
@conn.db.destroy @database, (err, res) ->
err = null if err? and err.statusCode is 404 and ignoreMissing
d.makeNodeResolver()(err, res)
d.promise
# Returns promise of inserting a doc
_insertDoc: (doc) ->
d = Q.defer()
@conn.use(@database).insert doc, d.makeNodeResolver()
d.promise
# Returns promise of CouchDB view
_view: (designDoc, view, params={}) ->
d = Q.defer()
@conn.use(@database).view designDoc, view, params, d.makeNodeResolver()
d.promise
module.exports = DAL
What’s missing is the endpoint and test implementation. Let’s start with the tests.
diff --git a/specs/sofas.spec.coffee b/specs/sofas.spec.coffee
index 8c1eb84..efbff58 100644
--- a/specs/sofas.spec.coffee
+++ b/specs/sofas.spec.coffee
@@ -1,13 +1,54 @@
-should = require 'should'
+should = require 'should'
+DAL = require '../dal'
+request = require 'supertest'
+request = request "http://localhost:#{process.env.PORT}"
+server = require '../server'
+
+app = null
describe 'Sofas API', ->
+ beforeEach (done) ->
+ # Reset database and start express server before each test
+ dal = new DAL(process.env.COUCH_PORT, process.env.DATABASE)
+ dal.resetDatabase(true)
+ .then ->
+ app = server.listen done
+ .fail (err) -> done err
+
+ # Close express server after each test
+ afterEach (done) ->
+ app.close done
+
+
describe '/sofas', ->
- it 'should return all sofas with GET', ->
- # TODO: write test
- (false).should.be.true()
+ it 'should return all sofas with GET', (done) ->
+ request
+ .get '/sofas'
+ .expect 200
+ .expect (res) ->
+ sofas = res.body
+ sofas.should.have.length(3)
+ for s in sofas
+ s.should.have.property('_id')
+ s.should.have.property('_rev')
+ s.should.have.property('name')
+ s.should.have.property('price')
+
+ .end done
+
+
describe '/sofas/:sofa', ->
- it 'should return a particular sofa with GET', ->
- # TODO: write test
- (false).should.be.true()
+ it 'should return a particular sofa with GET', (done) ->
+ request
+ .get '/sofas/sofa1'
+ .expect 200
+ .expect (res) ->
+ sofa = res.body
+ sofa.should.have.property('_id')
+ sofa.should.have.property('_rev')
+ sofa.should.have.property('name')
+ sofa.should.have.property('price')
+
+ .end done
Now the endpoints:
diff --git a/server.coffee b/server.coffee
index 98f1918..a0d30ce 100644
--- a/server.coffee
+++ b/server.coffee
@@ -1,16 +1,27 @@
express = require 'express'
+DAL = require './dal'
+dal = new DAL(process.env.COUCH_PORT, process.env.DATABASE)
app = express()
app.get '/sofas', (req, res) ->
- throw new Error 'not implemented'
+ dal.getSofas()
+ .then (sofas) ->
+ res.json(sofas)
+ .fail (err) -> res.status(err.statusCode).send()
app.get '/sofas/:sofa', (req, res) ->
- throw new Error 'not implemented'
+ dal.getSofas(req.params.sofa)
+ .then (sofas) ->
+ if sofas.length isnt 1
+ res.status(404).send()
+ else
+ res.json(sofas[0])
+ .fail (err) -> res.status(err.statusCode).send()
exports.listen = (callback) ->
At this point we can execute gulp test-int command to run tests on real CouchDB:
gulp test-int
Sofas API
/sofas
✓ should return all sofas with GET
/sofas/:sofa
✓ should return a particular sofa with GET
2 passing (204ms)
mock-couch tests
So how do we use mock-couch for this? Mock-couch is an HTTP server based on Restify. Note that Node.js runtime is single-threaded therefore we won’t be able to run both express and mock-couch at the same time. However, it is possible to run mock-couch on another process.
Node provides a couple of ways to create processes out of the box. We’ll use one called forking: not only we’ll be able to control the lifecycle of the forked child process but we’ll also have the ability to send and receive messages between parent and the child.
OK, less talk and more forking. We’ll do the forking in gulp test task:
diff --git a/gulpfile.coffee b/gulpfile.coffee
index cbd61b5..1200834 100644
--- a/gulpfile.coffee
+++ b/gulpfile.coffee
@@ -6,14 +6,48 @@ mocha = require 'gulp-mocha'
sources =
tests: './specs/**/*.spec.coffee'
+
# Task to run tests with mock-couch backend
gulp.task 'test', ->
process.env.NODE_ENV = 'test'
process.env.PORT = 3010
process.env.COUCH_PORT = 5987
process.env.DATABASE = 'sofas_test'
- gulp.src(sources.tests)
- .pipe(mocha())
+
+ fork = require('child_process').fork
+ couchProcess = fork('node_modules/gulp/bin/gulp.js', ['mock-couch'], {cwd: __dirname})
+ couchProcess.on 'message', (msg) =>
+ if msg is 'listening'
+ gulp.src(sources.tests)
+ .pipe(mocha())
+ .once('error', (err) ->
+ process.exit(1)
+ )
+ .once('end', ->
+ process.exit()
+ )
+
+ process.on 'exit', ->
+ couchProcess.kill()
+
+gulp.task 'mock-couch', ->
+ process.env.NODE_ENV = 'test'
+ process.env.PORT = 3010
+ process.env.COUCH_PORT = 5987
+ process.env.DATABASE = 'sofas_test'
+
+ mockCouch = require 'mock-couch'
+ couchdb = mockCouch.createServer()
+ DAL = require './dal'
+ dal = new DAL(process.env.COUCH_PORT, process.env.DATABASE)
+ docs = [dal._sofaViews]
+ docs = docs.concat dal._sofaSamples
+
+ couchdb.addDB process.env.DATABASE, docs
+
+ couchdb.listen process.env.COUCH_PORT, ->
+ process.send 'listening'
+
# Task to run integration tests (real CouchDB backend)
gulp.task 'test-int', ->
Also, we need a slight change to the test setup:
diff --git a/specs/sofas.spec.coffee b/specs/sofas.spec.coffee
index efbff58..93e7bd2 100644
--- a/specs/sofas.spec.coffee
+++ b/specs/sofas.spec.coffee
@@ -9,12 +9,17 @@ app = null
describe 'Sofas API', ->
beforeEach (done) ->
- # Reset database and start express server before each test
- dal = new DAL(process.env.COUCH_PORT, process.env.DATABASE)
- dal.resetDatabase(true)
- .then ->
- app = server.listen done
- .fail (err) -> done err
+
+ if process.env.NODE_ENV is 'test-int'
+ # Reset database and start express server before each test
+ dal = new DAL(process.env.COUCH_PORT, process.env.DATABASE)
+ dal.resetDatabase(true)
+ .then ->
+ app = server.listen done
+ .fail (err) -> done err
+ else
+ app = server.listen done
+
# Close express server after each test
afterEach (done) ->
Moment of Truth
If you try running gulp test
right now, you should see similar output:
gulp test
Sofas API
/sofas
✓ should return all sofas with GET (125ms)
/sofas/:sofa
✓ should return a particular sofa with GET (41ms)
2 passing (178ms)
As you can see the tests completed slightly faster than with real CouchDB. In a small code base like this, it’s hardly worth switching to mock-couch, if execution speed is your only motivation. However, with bigger codebases, the speed improvement can become very apparent.
Besides speed, there’s another benefit to this approach: you don’t actually need CouchDB to test your project. The simplified build process is especially useful with Continuous Integration platforms like Travis CI and Codeship.
Comments