TLDR
There are two options:
- Continue testing as a whole with functional tests (because these
as a whole
tests are not unit tests). You can feel this is not the right way, but it can be the only way to go with your current code base.
- Refactor code to smaller units (classes / functions) to make real unit testing possible. Then create unit tests for these smaller "units".
Long Explanation
First, to make sure we are talking about the same, the example of the controller code:
// Controller code
exports.changePassword = function (req, res, next) {
// Init Variables
var passwordDetails = req.body;
var message = null;
if (req.user) {
if (passwordDetails.newPassword) {
User.findById(req.user.id, function (err, user) {
if (!err && user) {
if (user.authenticate(passwordDetails.currentPassword)) {
if (passwordDetails.newPassword === passwordDetails.verifyPassword) {
user.password = passwordDetails.newPassword;
user.save(function (err) {
...
res.send({ message: 'Password changed successfully' });
});
} else {
res.status(400).send({ message: 'Passwords do not match' });
}
} else {
res.status(400).send({ message: 'Current password is incorrect' });
}
} else {
res.status(400).send({ message: 'User is not found' });
}
});
} else {
res.status(400).send({ message: 'Please provide a new password' });
}
} else {
res.status(400).send({ message: 'User is not signed in' });
}
};
And the test code:
// Change password
...
// agent performs the http request to the application
agent.post('/api/users/password')
.send({
newPassword: '1234567890Aa$', ...
})
.expect(200)
.end(function (err, res) {
...
res.body.message.should.equal('Password changed successfully');
return done();
});
This approach works, but these tests are actually not unit tests, but functional tests which verify the system as a black-box.
That means they are slower and less granular.
So how can we write unit tests for the controllers?
With the existing structure it can be done like this (this is not a real code, just to demonstrate the idea):
var users = require('../controllers/users.server.controller');
describe('User Controller Unit Tests:', function () {
describe('Change Password', function () {
it('should change the password', function (done) {
req = RequestMock({
'body': {
'newPassword': 'xxx', 'passwordVerify': 'xxx'
}
);
res = ResponseMock();
users.changePassword(req, res).then(function() {
res.body.message.should.equal('Password changed successfully');
});
});
We get closer to the unit test and now we have request / response mocks and call our changePassword
directly, but we still test it as a whole.
The test structure didn't change much.
The solution is to make the code testable first, re-factor it to extract the units
from the changePassword
code, which now does too much:
- Get the incoming data from the request
- Validate incoming data
- Change the password
- Handle possible errors
- Format the output
- Send the output
The better code structure could contain following objects:
- Controller - get the incoming data from the request, format and send the output
- NewPasswordConstraints - validate / verify incoming data
- User - change the password
The controller code could be something like this:
// Controller code
exports.changePassword = function (req, res, next) {
// Get the incoming data
var passwordDetails = req.body;
// Validate the incoming data
NewPasswordConstraints(passwordDetails).validate().then(function(user) {
// Actually change the password
return user.set_new_password(passwordDetails.newPassword);
}).then(function(user) {
// Format and send the success response
res.send({ message: 'Password changed successfully' });
// Format and send the error response
}).catch(function(error) {
res.status(error.code).send({ message: error.message });
});
}
Again, this is more a pseudo-code than real code, but I think the idea should be clear.
Now you can create unit tests for the business logic - NewPasswordConstraints and User classes.
At the same time, controller code becomes slim and only integrates other objects. I usually don't write unit tests for these 'slim' controllers and leave them for the functional tests.