In the previous article we saw how to test GET
transactions in our API. There's still a lot to cover so in this article we are going to be covering how to test POST operations; in other words how to test create.
Perhaps it is time we start POSTing stuff to our app.
Testing POST
Picking from the work we've already laid on the previous articles, let's move on straight into writing our expectations for creating our "post". Let's write our expectations.
First, to keep things simple, we are not doing any validation on the Posts model. You can create empty posts if you like right now, but we really don't care in this article. All we care for is that you send the proper captcha.
In a future article we are going to do validations, but we are going to do it with custom errors and handlers, which is pretty cool actually.
Back to the subject, if you send an invalid captcha, you should get a 400 Bad Request
with the message 'Invalid Captcha'. If all is good, you should get a 201 Created
, with the recently created post. Let's write our tests then.
// spec/api/functional/PostsSpec.js
describe('Posts endpoint', function(){
function clear() {
wolfpack.clearResults();
wolfpack.clearErrors();
}
beforeEach(clear);
// ... All our previous tests should be here ... //
describe('POST /posts', function(){
var stub;
beforeEach(function(){
clear();
stub = sinon.stub(Captcha, 'verifyCaptcha');
Posts.create.reset();
});
afterEach(function(){
stub.restore();
});
it("should return 400 and Invalid captcha if captcha verification fails", function(done){
stub.returns(false);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(function(res){
// Verify post was not created
if (Posts.create.called) { throw Error('Post should have not been created'); }
})
.expect(400, {message: 'Invalid captcha'}, done);
});
it("should return 201 and the created post", function(done){
stub.returns(true);
wolfpack.setCreateResults(fixtures.posts[0]);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(201, fixtures.posts[0], done);
});
});
});
Tests seem to increase in complexity from what we saw in the previous article right? Go figures :P Anyway let's analyze the setup.
describe('POST /posts', function(){
var stub;
beforeEach(function(){
clear();
stub = sinon.stub(Captcha, 'verifyCaptcha');
Posts.create.reset();
});
afterEach(function(){
stub.restore();
});
// ... Our tests are here ... //
});
Let's start with the stub. What is that? The Captcha is going to be handled by a service we are going to create called Captcha
. This service will have a method called verifyCaptcha
that will do all the captcha processing and return either true or false, depending whether it succeeded or not. Obviously we need to test both scenarios so we created a stub for it which will act as the Captcha.verifyCaptcha
.
We also need to reset the call counter for Posts.create
. Wolfpack kindly enough injected Sinon into the create
method so that we can know if it was called, so we must make sure the counter is always reset to 0 when we perform the tests.
And let's not forgot the clear
on top that resets any results and errors we've previously set on wolfpack.
For the teardown of the test (the afterEach), we make sure that we restore the stubbed function to its original state.
Let's see the tests now.
it("should return 400 and Invalid captcha if captcha verification fails", function(done){
stub.returns(false);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(function(res){
// Verify post was not created
if (Posts.create.called) { throw Error('Post should have not been created'); }
})
.expect(400, {message: 'Invalid captcha'}, done);
});
The first thing we do is set our stub for the captcha to return false. That's the stub.returns(false)
thing. Afterall, we want to test that the captcha failed, so the method should return false.
Next, we send our request to the server. We POST
to /posts
, and then send in the body of the request. Again, it is a bad practice to send the fixture as the body. We should type in what we are sending via the POST
, and if this changes in the future, we need to update the spec accordingly, just as we did here.
Unlike expectations, which can be placed in a separate file, it is good practice to have the body parameters located where the test is written. This allows for better visibility as to what are the conditions for the test to either fail or pass.
Sending the fixture as the body and the results of an operation can bring problems in the future as it automatically changes an expectation, which should fail in the first place for the change. In fact, in a little bit you are going to see why.
Moving on to the actual test expectations, the first expectation we set with Supertest was to check that if the captcha failed, no post should be created. For that, we are going to check that the spy wolfpack injected into the Posts model's create
method was not called in the operation. If it is called, we raise the exception with our own message.
If Posts.create
was not called, we can then check that we got the 400
HTTP status, and that we get a JSON with the message Invalid captcha.
Time to account for when the post is created successfully.
it("should return 201 and the created post", function(done){
stub.returns(true);
wolfpack.setCreateResults(fixtures.posts[0]);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(201, fixtures.posts[0], done);
});
So for starters, we are making our stub for the captcha return true. So basically, our captcha test is passing correctly.
Next, we are setting up the database results Sails is going to get when we create the post. We do that using the global wolfpack.setCreateResults
, which will set the same result for all create operations. Since there is only one create operation, no need to worry here. In more complex operations where there is more than one create, we can use additional features of wolfpack that will allow us to test.
Now we make the POST
request to the server. This time we don't have anything else we need to keep track like if Posts.create
was called, although we can do that. We could also make sure that our stub is actually being called and that the verifyCaptcha
operation indeed happened, but I'll let those as a homework for you. So right now we only test that we get the 201 Created
, and the post in return. Again, the way I did it here is the bad practice. Do it as I told you to do it.
Let's run the tests and see them fail. Great! They are red! Time to fix that.
First, let's create our Captcha
service. Since this is just a demo, I won't really be implementing a Captcha. I'm just gonna create the service and method, and make it fail miserably.
Create a new service under api/services/Captcha.js and paste the following:
// api/services/Captcha.js
module.exports = {
verifyCaptcha: function() {
throw Error('This error means you didn\'t stub the captcha correctly in your tests');
}
};
Now let's write the code in our controller that will handle the route. The code below is the complete code that is contained within our controller (all route handlers included):
// api/controllers/PostsController.js
module.exports = {
//.. All the GET stuff from the previous article here ..//
createPost: function(req, res) {
var body = {
title: req.body.title,
content: req.body.content,
author: req.body.author
};
var valid = Captcha.verifyCaptcha(req.body.captcha);
if (!valid) {
return res.json(400, {message: 'Invalid captcha'});
}
return Posts.create(body).then(function(post){
return res.json(201, post);
}).catch(function(err){
return res.json(500, err);
});
}
};
Noticed the scenario I coded but we are not testing? Yes, the 500
scenario. We are going to test that afterwards. Let's finish with the create
first.
Now let's update our routes file. This is the complete route file with all routes included:
// config/routes.js
module.exports.routes = {
'GET /posts': {
controller: 'PostsController',
action: 'getAllPosts'
},
'GET /posts/:id': {
controller: 'PostsController',
action: 'getPost'
},
'POST /posts': {
controller: 'PostsController',
action: 'createPost'
}
};
Now let's run the tests. All green! Yay! Well, no. My dear friend, you just got a bad positive for setting fixtures as results.
The problem lies that in the setup you did (well I actually did it, but you get the point). The 400
test about the captcha is good, but the 201
test for creating the post is a bad positive.
Let's look at the setup of the test. We set the create results with Wolfpack:
wolfpack.setCreateResults(fixtures.posts[0]);
Wolfpack's setCreateResults
is a very powerful method that if used wrong it can bring false positives. Check now your expectation:
.expect(201, fixtures.posts[0], done);
So basically we are telling to expect fixtures.posts[0] === fixtures.posts[0]
. The setCreateResults
method ignores whatever data you sent in the body. It simply returns the exact data that you told him to return when the create
in a model is called.
If you do not provide a setCreateResults
, wolfpack will return the data processed through all hooks as if it was stored in the db. Comment the wolfpack.setCreateResults(fixtures.posts[0])
line and run the tests again and see the actual results.
See? We had a false positive because we rewrote the db results and we set the same fixture as the expectation.
This tells us two things. First, our fixture is wrong and is not representing the actual data as it is in the database, so we need to update it, and second, it is a terrible idea to use the fixture as an expectation.
First let's update our fixture with the two missing fields from the db: createdAt
and updatedAt
. Our posts fixture will now look like this:
// spec/fixtures/posts.js
module.exports = [
{
id: 1,
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare',
createdAt: '2015-11-01T12:00:00.001Z',
updatedAt: '2015-11-01T12:00:00.001Z'
},
{
id: 2,
title: 'My first poem',
content: 'Where is the question mark!"#$%&/()=?',
author: 'Sappho',
createdAt: '2015-11-01T12:00:00.002Z',
updatedAt: '2015-11-01T12:00:00.00Z'
}
];
By the way, the createdAt
and updatedAt
are just random dates I picked to make writing the expectation easier.
We also need to update the counters fixtures because they also have createdAt
and updatedAt
values in the database:
// spec/fixtures/counters.js
module.exports = [
{
id: 1,
postId: 1,
count: 3,
createdAt: '2015-11-01T12:00:00.001Z',
updatedAt: '2015-11-01T12:00:00.001Z'
},
{
id: 2,
postId: 2,
count: 1,
createdAt: '2015-11-01T12:00:00.002Z',
updatedAt: '2015-11-01T12:00:00.002Z'
}
];
Now we need to update our expectations and work them properly. It is a good practice to put the expectation in a separate file, but for this article, I'll just put it within the same test:
it("should return 201 and the created post", function(done){
stub.returns(true);
wolfpack.setCreateResults(fixtures.posts[0]);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(201, {
id: 1,
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare',
createdAt: '2015-11-01T12:00:00.001Z',
updatedAt: '2015-11-01T12:00:00.001Z'
}, done);
});
Finally, let's run the tests one more time and see them turn green.
grunt test
Red? What happened? Of course! Since the fixture changed, our expectation is different. We are now getting updatedAt
and createdAt
in our GET /posts/:id
operation, which is good because it means our tests caught a breaking change.
Go ahead and update that expectation (I'll leave it up to you) and run the tests and watch them turn green.
There you go! Everything is green, including our previous tests! Yeah, I'm not gonna let you go that easily. Did you update the expectation in GET /posts
? No? Well that's because you made the expectation the same as the fixture. The moment you changed the fixture, that test should have also break, but it didn't, which means a false positive. Go ahead and fix that so the expectation is not the fixture.
I hope this has been enough to show you why it is very important to have an expectation separate from a fixture. It can save you in the future. It should also have shown you the importance of knowing your data. Wolfpack is very powerful, and if used incorrectly, it can give you false results.
Now, onto testing the error condition.
If you recall, we have a special handler for 500
errors in our controller:
//.. some code above for our captcha stuff ../
return Posts.create(body).then(function(post){
return res.json(201, post);
}).catch(function(err){
return res.json(500, err);
});
So how can we test error an Internal Server error condition? Fortunately, wolfpack provides us with a useful method for mocking errors in the backend called setErrors
.
First let's write our new expectation in our tests. We already have the code so we only need the expectation which is basically to show an error message in a specific format if there is any unknown error. Let's write it then:
// spec/api/functional/PostsSpec.js
describe('POST /posts', function(){
var stub;
beforeEach(function(){
clear();
stub = sinon.stub(Captcha, 'verifyCaptcha');
Posts.create.reset();
});
afterEach(function(){
stub.restore();
});
// .. all our previous POST tests here ..//
it("should return 500 and the error message if there is an unknown error", function(done){
stub.returns(true);
var errorMessage = 'Some error happened';
wolfpack.setErrors(errorMessage);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(500, {error: errorMessage}, done);
});
});
For starters, the error happens in Post.create
. Than means we need to get past through the captcha validation first, so we set the stub for the captcha to return true.
Next, we tell wolfpack to return an error for any operation performed. The setErrors
method will override any CRUD operation and immediately return an error when a CRUD is called.
Now we perform our call to the server with our expectation. Run it and you will see it will fail because, even though we are getting a 500 error, the message is not the proper format. We need to update it.
So, go back to the PostsController.js, and in the error handler for the create, update the code to the proper format:
// api/controllers/PostsController.js
module.exports = {
//.. All the GET stuff from the previous article here ..//
createPost: function(req, res) {
var body = {
title: req.body.title,
content: req.body.content,
author: req.body.author
};
var valid = Captcha.verifyCaptcha(req.body.captcha);
if (!valid) {
return res.json(400, {message: 'Invalid captcha'});
}
return Posts.create(body).then(function(post){
return res.json(201, post);
}).catch(function(err){
return res.json(500, {error: err.originalError});
});
}
};
Now we run our tests again and everything is green. Great! We've achieved better coverage and our api is fully tested, even for when errors happen.
Just for review, this is how the tests for our POST will look like then:
describe('POST /posts', function(){
var stub;
beforeEach(function(){
clear();
stub = sinon.stub(Captcha, 'verifyCaptcha');
Posts.create.reset();
});
afterEach(function(){
stub.restore();
});
it("should return 400 and Invalid captcha if captcha verification fails", function(done){
stub.returns(false);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(function(res){
// Verify post was not created
if (Posts.create.called) { throw Error('Post should have not been created'); }
})
.expect(400, {message: 'Invalid captcha'}, done);
});
it("should return 201 and the created post", function(done){
stub.returns(true);
wolfpack.setCreateResults(fixtures.posts[0]);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(201, {
id: 1,
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare',
createdAt: '2015-11-01T12:00:00.001Z',
updatedAt: '2015-11-01T12:00:00.001Z'
}, done);
});
it("should return 500 and the error message if there is an unknown error", function(done){
stub.returns(true);
var errorMessage = 'Some error happened';
wolfpack.setErrors(errorMessage);
request(server)
.post('/posts')
.send({
title: 'My First Novel',
content: 'This is my novel. The end.',
author: 'William Shakespeare'
})
.expect(500, {error: errorMessage}, done);
});
});
What's next?
We've finished our tests for GET
and POST
transactions. Still, there are two more common verbs we need to cover: PUT
and DELETE
. That's what we are going to be seeing in the next article. And after that? Sails Sessions and Policies :)
And remeber, the project for this article is available at:
https://github.com/fdvj/sails-testing-demo