Testing MEAN apps, Part 1 20 Mar 2014

Me no like unit tests

As I wrote in my Lessons from Production posts, tests are sexy. There’s no getting around that. They let you know everything works, and pushing your updates won’t lose you any customers.

When building MEAN apps though, you need to write a few different types of tests. Sure, you probably already know about the difference between unit and integration tests, but unit tests for Angular code are dramatically different from unit tests for Mongoose models or Node code.

I’m going to run through what you need to know to effectively test MEAN stack applications, mostly as a brain dump of what I’ve learned in the past ~15 months. Including everything in one post would get long, so I’ll split it up into a few dramatic installments.

The Pitfall Of Testing Assumptions

To begin with, don’t test assumptions. Actually, don’t even go so far as to make assumptions that are testable. You should test all parts of your codebase with equal vigilance. Don’t fall into the trap of writing tests for the component you assume is most likely to fail, and ignoring others. The point of testing is to catch issues you don’t see coming.

If you have a chunk of functionality you assume will break in the future, don’t write more tests for it, just fix it! Writing tests doesn’t mean you want them to fail every time you make a change. That’s just a sign of a crappy codebase.

One does not simple write unit tests

Clean Code Is Testable Code

Before you can start laying down swathes of tests, you need to evaluate how difficult it will be to write those tests in the first place. If you are dealing with spaghetti code and tightly coupled modules, you won’t have a fun time.

Hopefully, you don’t find yourself in such a situation. Even if you feel the bridge calling each time you open a source file, there is still hope. Refactoring a large codebase all at once is a terrible idea, but that doesn’t mean you can’t tweak things as you move along. (I don’t know much about effectively refactoring legacy code, but you may want to read this book.)

In general, you want to keep your code modular and loosely coupled. Testing one method should not require mocking more than a single module. If you find yourself having to bring in or mock half of your codebase to test a handleful of routines, you aren’t doing it right.

I try to stick to the “one method does one thing” philosophy in my code, breaking logical chunks of functionality into small, testable methods. Not only are short methods easier to read and test, but they are also easier to follow for new developers and mean you can write less comments (assuming you use proper naming).

You may also want to check out the Law of Demeter.

Learning The Testing Lingo

I test with a combination of Mocha and Chai, along with PhantomJS for client-side tests. Mocha is a testing framework, while Chai is an assertion library, providing all kinds of cool test methods in a few different flavors.

Use Mocha to describe your test suite, by splitting it up into tests for specific modules, sub-modules, and methods. Each test is wrapped in an it() block, as follows:

 1 describe "A very important module", ->
 2 
 3   ....
 4   describe "Submodule #33", ->
 5 
 6     ...
 7     describe "reallyAwesomeMethod", ->
 8       it "does something really awesome", ->
 9 
10         #
11         # Perform your actual testing in here with Chai
12         #
13 
14     ...
15 
16   ...

Chai provides two primary assertion styles, should and expect. Should tests that an object should have a specific quality, while expect tests that an object has an expected quality. Again, more examples:

 1 require("chai").should()
 2 expect = require("chai").expect
 3 
 4 # ...
 5 # Inside of a Mocha it() block
 6 expect(someObject).to.exist
 7 expect(anArray).to.exist
 8 
 9 someObject.should.have.property "awesome"
10 someObject.awesome.should.equal "3"
11 
12 anArray.should.have.length 4

The main difference between the two is that should extends the Object prototype, meaning you need to execute it at the top as shown, and can only be called on objects that exist.

Also, I find that testing properties with .should.have.property “prop” yields more useful error messages than expect(object.prop).to.exist.

Mocha comes with a bunch of different reporters, which control how the test results are displayed, and has a few more awesome features. We’ll go over a bit more of its functionality down below.

Writing Server-Side Unit Tests

So, onto the tests themselves!

Testing mongoose models

To begin with, let’s talk about models. When coding a cleanly seperated client/server system, I vouch to keep as much functionality as possible on my models. Aka, if there is any processing you need to do with a models’ data after fetching it from the database, and before responding to the active request, do that processing within the model.

In other words, this:

 1 app.get "/api/v1/pancakes/:id", requireLogin, (req, res) ->
 2   db.model("Pancake").findById req.params.id, (err, pancake) ->
 3     return res.send 500 if err
 4     return res.send 404 unless pancake
 5 
 6     ret = pancake.toObject()
 7     ret.id = ret._id.toString()
 8 
 9     delete ret._id
10     delete ret.__v
11     delete ret.someSecureField
12 
13     res.json ret

Turns into this:

1 app.get "/api/v1/pancakes/:id", requireLogin, (req, res) ->
2   db.model("Pancake").findById req.params.id, (err, pancake) ->
3     return res.send 500 if err
4     return res.send 404 unless pancake
5 
6     res.json pancake.toAPI()

Where you have a toAPI() method on your model that looks like this:

 1 ###
 2 # Convert model to API-safe object
 3 #
 4 # @return [Object] apiObject
 5 ###
 6 schema.methods.toAPI = ->
 7   ret = @toObject()
 8   ret.id = ret._id.toString()
 9 
10   delete ret._id
11   delete ret.__v
12   delete ret.someSecureField
13 
14   ret

Why is this so awesome, besides breaking out useful functionality wherever you have a model? You can test those methods! Check this out:

 1 should = require("chai").should()
 2 expect = require("chai").expect
 3 mongoose = require "mongoose"
 4 require "../../../../src/server/models/Pancake"
 5 
 6 model = mongoose.model "Pancake"
 7 
 8 describe "Pancake Model", ->
 9 
10   describe "toAPI" ->
11     it "strips the model of sensitive information", ->
12       pancake = model()
13 
14       pancake.should.have.property "toAPI"
15 
16       apiObject = pancake.toAPI()
17 
18       apiObject.should.have.property "id"
19       apiObject.should.not.have.property "_id"
20       apiObject.should.not.have.property "__v"
21       apiObject.should.not.have.property "someSecureField"


Actually setting up model tests

Of course, in order to actually experience any of this magic, you need to connect to your Mongo test database and load models before the tests. I define models in files that get required’ during the initialization procedures of my projects. If this is your model file:

 1 mongoose = require "mongoose"
 2 
 3 schema = new mongoose.Schema
 4   topping: String
 5   meaning: { type: Number, default: 42 }
 6 
 7 #
 8 # Any methods you may have
 9 #
10 
11 mongoose.model "Pancake", schema

Then you can set up your tests by just scanning the model directory for files, and requiring them up one at a time. Mocha breaks out before(), beforeEach(), after() and afterEach() methods for tests, so any DB init should be performed within a before() block. Specifically:

 1 mongoose = require "mongoose"
 2 fs = require "fs"
 3 
 4 before (done) ->
 5   con = "your_uber_secure_connection_string"
 6   modelPath = "#{__dirname}/where/am/I"
 7 
 8   mongoose.connect con, (err) ->
 9 
10     # Setup db models
11     fs.readdirSync(modelPath).forEach (file) ->
12       if ~file.indexOf ".coffee"
13         require "#{modelPath}/#{file}"
14 
15     done()
16 
17 # These are files containing your model tests
18 require "./models/pancake"


Unit tests for the rest of your backend

Besides mongoose models (which have to be loaded up with a DB connection), testing standard Node code is fairly straightforward.

Usually, as long as you don’t have other dependencies that you want to keep out of tests, you can just require your modules and test each method individually.

If, however, you need to mock dependencies that are loaded in with a require() call, take a look at mockery. Mockery lets you supply your own objects to pass through to required() files. You should enable mockery befor each individual test, and tear it down afterwards like so:

 1 mockery = require "mockery"
 2 
 3 beforeEach ->
 4   mockery.enable
 5     warnOnReplace: false
 6     warnOnUnregistered: false
 7     useCleanCache: true
 8 
 9 afterEach ->
10   mockery.deregisterAll()
11   mockery.disable()

Then, build and register a mock object before requiring your module:

 1 describe "Module A", ->
 2   describe "someTestableMethod", ->
 3 
 4     it "fetches a model and calls doSomething() on it", (done) ->
 5 
 6       # Mock mongoose
 7       mongoose = model: (name) ->
 8         expect(name).to.equal "ourModel"
 9 
10         # Mock the find() method, to supply a rigged model
11         find: (query, cb) ->
12 
13           # Expect the module method to call doSomething() on our model
14           riggedModel = doSomething: -> done()
15 
16           # Return null in the error field
17           cb null, riggedModel
18 
19       mockery.registerMock "mongoose", mongoose
20 
21       # Now load our module, call the method, and hope for the best
22       ourModule = require "../../../src/server/api/pancakes"
23 
24       # This is really lame, but you get it
25       ourModule.doSomethingWith "ourModel"


Wrapping up

Wasn’t that awesome? :D

As this post is starting to get quite long, I’ll go ahead and wrap it up here :) This is only the tip of the iceberg; We’ll talk about client-side Angular unit tests (which are quite a bit messier than what we’ve looked at so far), and go over full integration tests next time.

In part three, I’ll talk a bit about hooking up tests to run automatically and integrating them into a deployment pipeline.

Keep testing!