Supertest: Verify database after request

· May 5, 2015

One thing that I often find myself want to do is to check the database after I have created a resource. For example:

  • Post some data to /user
  • Verify that I get 201 Created back
  • Check that the data in the database looks ok

I have had such a hard time finding a stable way to do this. I know that I have a little bit of a special tool chain but still… it should not be this hard.

But last night, after weeks searching for this, I got it to work. I’m so excited to share this with you.

Tools

I’m using the following tools:

If you want to tag along as I build this example out, grab the code from this tag.

The start

Here’s the test I’m starting from, you’ll find it in /test/user.post.js:

describe('POST to /user', function(){

	var test_user = {};

	beforeEach(function (done) {
		test_user = helpers.test_user;
		helpers.removeAll(done);
	});

	afterEach(function (done) {
		helpers.removeAll(done);
	});

	it('creates a new user for complete posted data', function(done){
		// Post
		request
			.post('/user')
			.send(test_user)
			.expect('location', /^\/user\/[0-9a-fA-F]{24}$/) // Mongo Object Id /user/234234523562512512
			.expect(201, done);
	});

	// and some other tests
});

There’s a testHelpers.js file where the test_user object is defined.

We clean the database, before and after each test (lines 5-12). Overkill maybe but at least clean

The interesting part is of course the test from lines 14-21. It’s a good test. Well-defined and even checks the location returned with a regular expression to match a MongoDb Id. But… this could still not have put any data into the database. It’s unlikely but we don’t know. I don’t like that.

Running the tests npm test, before we start, makes sure that this work.

... other tests ...

POST to /user
✓ creates a new user for complete posted data

... other tests ...

6 passing (100ms)

Ah well… I changed the status code from 200 to 201 in the example so that needs to be updated in the userRoute.js… but then it works.

The humble start … by using .end()

What we want is to after the request has finished check the state of the database. And supertest actually exposes a excellent place to do that; .end().

And that takes a function. Let’s by just using the end function. Like this:

it('creates a new user for complete posted data', function(done){
	// Post
	request
		.post('/user')
		.send(test_user)
		.expect('location', /^\/user\/[0-9a-fA-F]{24}$/) // Mongo Object Id /user/234234523562512512
		.expect(201)
		.end(done);
});

This is just making the test a little bit clearer to read. “And then end the request and test by calling done” was basically what we said.

Rerunning the test to make sure it works, of course.

Our own function

But we wanted to do something after the request as ended. Luckily we can by adding a function of our own as parameter to the .end(). Here’s the trivial example of that:

.end(function () {
	done();
});

Too trivial. Let’s instead look up some data and verify that the test_user.name actually has been set in the database. Here’s what we want to say:

var userFromDb = yield users.findOne({ name: test_user.name});
userFromDb.name.should.equal(test_user.name);

(The users collection is defined in testHelpers and in our file we have a the following at the top, for easy access var helpers = require('./testHelpers.js'); var users = helpers.users;.

Note that you have to require and install should to make this work)

Ha! Trivial again… I said… Until I remember that yield cannot be called in a non-generator function. Let’s try and you’ll see. Here’s the code that will not work:

.end(function (){
	var userFromDb = yield users.findOne({ name: test_user.name});
	userFromDb.name.should.equal(test_user.name);
	done();
});

Running the tests again (npm t) and will get this error that I’ve blogged about before

var userFromDb = yield users.findOne({ name: test_user.name});
				                       ^^^^^^^^^^
SyntaxError: Unexpected identifier

Basically it tries to say: “I don’t know what you mean ‘yield’” but it cannot express itself particularly good.

yield can only be used in a generator function. A generator function is denoted with an asterisk and has to be called by some one that asks for the .next value it returns. mocha doesn’t do that. You can try by adding an asterisk at .end(function *(){ but it will just hang the test.

This is where co can help us. co is a little tool that simply brings:

Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way

Yes, exactly. I don’t get that either. Still. But I think I can use it. Because basically it means that you can wrap a generator function with co and then execute the co function as a normal function.

This is how it would look, after you have added a var co = require("co"); at the top of the file and npm install co --save-dev

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name: test_user.name});
		userFromDb.name.should.equal(test_user.name);
	})(done);
});

Detour - broken example code

You don’t have to read this part if you didn’t start from my code. But it will provide you with deeper understanding once you’re through it.

At this point my example code breaks ALL over… This has to do with it being based on old versions of the packages I’m using. I went through package.json and set the version to "*". For example: "co": "*",.

That’s a bit dangerous but works for now for me.

UPDATE: Too stupid

In fact, that is too dangerous. Let’s fix it properly instead. Another way is to remove all dependencies from the package.json and then reinstall them from the terminal. Make sure to use --save/--save-dev to store it in package.json.

I’ve updated my package.json in that manner now. Here’s the the two command I ended up running:

npm install koa koa-route co-body co-monk monk --save
npm install mocha co should supertest --save-dev

And now my package.json better represent the state I was actually running in at the time. Thank you Danny for that push to be better!

Back to the detour

Once that is done… all tests still fails. This time it has to do with co completely changing it’s behavior to return a promise. Luckily all the failures are in the same function: testHelpers.removeAll and can be solved by just moving the call to done inside the function, like this:

module.exports.removeAll = function(done){
	co(function *(){
		yield users.remove({});
		// and other things we need to clean up
		done();
	});
};

And now only 4 test still fail. These are the test using co in the old way. Most likely we invoke the co construction function directly (co(...)(); for example). This is not how it behaves now when co returns a Promise. The fix is just to remove the (); at the end of a call. Here’s an example from user.del.js

it('deletes an existing user', function(done){
	co(function *() {
		// Insert test user in database
		var user = yield users.insert(test_user);
		var userUrl = '/user/' + user._id;

		// Delete the user
		request
			.del(userUrl)
			.expect(200, done);
	}); // this line looked like this before: })();
});

And we’re back. Test are passing and we are using the latest version of our tools. Praise God for test when you update your infrastructure. And many other times too.

Back to the code at hand - let’s assert it

Before we run the tests, let’s go through the updated .end() function. Here it is again so that you don’t have to scroll:

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name: test_user.name});
		userFromDb.name.should.equal(test_user.name);
		done();
	});
});

There’s a couple of things to note:

  • On line 2 we wrap our generator function with co
  • Line 3 gets the userFromDb by calling our generator friendly users collection in testHelpers.js.
  • Line 4 checks the name to match the name of the test_user we posted. Admitting the check is a bit strange since we used that value to pull the data from the database too. But at least it would fail if we failed…
  • Line 5 calls done
  • And line 6 doesn’t invoke the function, since co returns a Promise. See above, if you dare.

npm test and it … fails?! We get a timeout… Now why.

Quick side note. I spent circa 2 months in this state. Not know what to do, hacking around, not verifying etc. etc. But I’ll spare you this. It was horrible. I don’t ever want to go back.

Promises, promises, promises

Remember that co returns a Promise?

... Now, co() returns a promise.

We can use that to our advantage, but let’s think for a short while. A Promise is basically saying “This will return… I promise… but not just yet.” That’s great in many many case and the killer feature of Node to start with, although implemented in Callbacks. Brrr… This is allows for asynchronous behavior and freeing up the main thread and all of that stuff that was what made us all love Node from the start.

But in a test we actually want it to execute now. And there’s an easy, and standardized way to do that: use the .then() function of the code implementing the Promise, which co does.

.then() just takes a function that we execute after the Promise has returned. done is a perfect candidate for us.

Here’s how it would look:

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name: test_user.name});
		userFromDb.name.should.equal(test_user.name);
	}).then(done);
});

AND. IT. WORKS! Tears of joy are streaming down my face as I realized…

Or did it… failing for the right reason

Well… for some reason I doubted this from time to time. And I changed the test to this:

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name: test_user.name});
		userFromDb.name.should.equal("This is not the name you are looking for");
	}).then(done);
});

Rerunning the test… and it hangs…

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

Yup… spent another two weeks here.

Well, it turns out that errors are not handled by .then() unless we tell it how. .then() takes two parameters, like this: .then(fn, fn), the first function gets called on success, the second on failure. When the test fails there’s not function to call and hence done() doesn’t get called.

That’s what the second parameter, also a function, does. Like this for example:

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name : test_user.name });
		userFromDb.name.should.equal("This is not the name you are looking for");
	}).then(done, function (err) {
		done(err);
	});
});

And now we get the failing test we (read: I) was looking for during all that time:

AssertionError: expected 'Marcus' to be 'This is not the name you are looking for'
+ expected - actual

+"This is not the name you are looking for"
-"Marcus"

Now I actually was crying. True story. But I was also on such a high that I got an idea. I seem to remember, from my time programming F#, that many functional languages have short cuts for functions that only take one parameter. Basically it’s passed implicitly or how you could explain it.

On a whim I just took the function out and changed it into this:

.end(function () {
	co(function *() {
		var userFromDb = yield users.findOne({ name : test_user.name });
		userFromDb.name.should.equal("This is not the name you are looking for");
	}).then(done, done);
});

Basically saying to the second done-parameter; take the err object and pass it to done, in this case which is failing the test.

And now it works AND is readable and short.

TL;DR; - summary.

I wanted to check the state of the database after doing a request. This can be done using the .end() function of supertest.

Since I used co-monk I wanted to be able to do that using yield and generators. This means that I need to wrap my generator function with co.

co, since version 4.0.0, returns a promise. This perfect for users of mocha since it allows us to use the .then() function and pass the done variable to both the success and failure functions of .then(fn success, fn failure(err)).

The test in it’s entirety is displayed below. My code is checked in here, under tag 1.2.

var co = require("co");
var should = require("should");
var helpers = require('./testHelpers.js');
var users = helpers.users;
var request = helpers.request;

describe('POST to /user', function(){

	var test_user = {};

	beforeEach(function (done) {
		test_user = helpers.test_user;
		helpers.removeAll(done);
	});

	afterEach(function (done) {
		helpers.removeAll(done);
	});

	it('creates a new user for complete posted data', function(done){
		// Post
		request
			.post('/user')
			.send(test_user)
			.expect('location', /^\/user\/[0-9a-fA-F]{24}$/) // Mongo Object Id /user/234234523562512512
			.expect(201)
			.end(function () {
				co(function *() {
					var userFromDb = yield users.findOne({ name : test_user.name });
					userFromDb.name.should.equal("This is not the name you are looking for");
				}).then(done, done);
			});
	});
});

YESSSSSS!

Twitter, Facebook