Blog

Mocking a REST backend for your AngularJS / Grunt web application

26 Jun, 2014
Xebia Background Header Wave

Anyone who ever developed a web application will know that a lot of time is spend in a browser to check if everything works as well and looks good. And you want to make sure it looks good in all possible situations. For a single-page application, build with a framework such as AngularJS, that gets all it’s data from a REST backend this means you should verify your front-end against different responses from your backend. For a small application with primarily GET requests to display data, you might get away with testing against your real (development) backend. But for large and complex applications, you need to mock your backend.
In this post I’ll go in to detail how you can solve this by mocking GET requests for an AngularJS web application that’s built using Grunt.

In our current project, we’re building a new mobile front-end for an existing web application. Very convenient since the backend already exists with all the REST services that we need. An even bigger convenience is that the team that built the existing web application also built an entire mock implementation of the backend. This mock implementation will give standard responses for every possible request.

Great for our Protractor end-to-end tests! (Perhaps another post about that another day.) But this mock implementation is not so great for the non standard scenario’s. Think of error messages, incomplete data, large numbers or a strange valuta. How can we make sure our UI displays these kind of cases correct? We usually cover all these cases in our unit tests, but sometimes you just want to see it right in front of you as well. So we started building a simple solution right inside our Grunt configuration.

To make this solution work, we need to make sure that all our REST requests go through the Grunt web server layer. Our web application is served by Grunt on localhost port 9000. This is the standard configuration that Yeoman generates (you really should use Yeoman to scaffold your project).

Our development backend is also running on localhost, but on port 5000. In our web application we want to make all REST calls using the /api path so we need to rewrite all requests to xebia.com/blog:9000/api to our backend: xebia.com/blog:5000/api. We can do this by adding middleware in the connect:livereload configuration of our Gruntfile.

[javascript]
livereload: {
options: {
open: true,
middleware: function (connect, options) {
return [
require('connect-modrewrite')(['^/api https://xebia.com/blog:5000/api [P L]']),
/* The lines below are generated by Yeoman */
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
[/javascript]

Do the same for the connect:test section as well.
Since we’re using ‘connect-modrewrite’ here, we’ll have to add this to our project:

npm install connect-modrewrite --save-dev

With this configuration every request starting will xebia.com/blog:9000/api will be passed on to xebia.com/blog:5000/api so we can just use /api in our AngularJS application. Now that we have this working, we can write some custom middleware to mock some of our requests.
Let’s say we have a GET request /api/user returning some JSON data:

[javascript]
{"id": 1, "name":"Bob"}
[/javascript]
Now we'd like to see what happens with our application in case the name is missing:
[javascript]
{"id": 1}
[/javascript]

It would be nice if we could send a simple POST request to change the response of all subsequent calls. Something like this:

curl -X POST -d '{"id": 1}' xebia.com/blog:9000/mock/api/user

We prefixed the path that we want to mock with /mock in order to know when we should start mocking something. Let’s see how we can implement this. In the same Gruntfile that contains our middleware configuration we add a new function that will help us mock our requests.

[javascript]
var mocks = [];
function captureMock() {
return function (req, res, next) {
// match on POST requests starting with /mock
if (req.method === 'POST' && req.url.indexOf('/mock') === 0) {
// everything after /mock is the path that we need to mock
var path = req.url.substring(5);
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('end', function () {
mocks[path] = body;
res.writeHead(200);
res.end();
});
} else {
next();
}
};
}
[/javascript]

And we need to add the above function to our middleware configuration:

[javascript]
middleware: function (connect, options) {
return [
captureMock(),
require('connect-modrewrite')(['^/api https://xebia.com/blog:5000/api [P L]']),
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
[/javascript]

Our function will be called for each incoming request. It will capture each request starting with /mock as a request to define a mock request. Next it stores the body in the mocks variable with the path as key. So if we execute our curl POST request we end up with something like this in our mocks array:

[javascript]
mocks['/api/user'] = '{"id": 1}';
[/javascript]

Next we need to actually return this data for requests to xebia.com/blog:9000/api/user. Let’s make a new function for that.

[javascript]
function mock() {
return function (req, res, next) {
var mockedResponse = mocks[req.url];
if (mockedResponse) {
res.writeHead(200);
res.write(mockedResponse);
res.end();
} else {
next();
}
};
}
[/javascript]
And also add it to our middleware.
[javascript]
...
captureMock(),
mock(),
require('connect-modrewrite')(['^/api https://xebia.com/blog:5000/api [P L]']),
...
[/javascript]

Great, we now have a simple mocking solution in just a few lines of code that allows us to send simple POST requests to our server with the requests we want to mock. However, it can only send status codes of 200 and it cannot differentiate between different HTTP methods like GET, PUT, POST and DELETE. Let’s change our functions a bit to support that functionality as well.

[javascript]
var mocks = {
GET: {},
PUT: {},
POST: {},
PATCH: {},
DELETE: {}
};
function mock() {
return function (req, res, next) {
if (req.method === 'POST' && req.url.indexOf('/mock') === 0) {
var path = req.url.substring(5);
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('end', function () {
var headers = {
'Content-Type': req.headers['content-type']
};
for (var key in req.headers) {
if (req.headers.hasOwnProperty(key)) {
if (key.indexOf('mock-header-') === 0) {
headers[key.substring(12)] = req.headers[key];
}
}
}
mocks[req.headers['mock-method'] || 'GET'][path] = {
body: body,
responseCode: req.headers['mock-response'] || 200,
headers: headers
};
res.writeHead(200);
res.end();
});
}
};
};
[/javascript]

 

[javascript]
function mock() {
return function (req, res, next) {
var mockedResponse = mocks[req.method][req.url];
if (mockedResponse) {
res.writeHead(mockedResponse.responseCode, mockedResponse.headers);
res.write(mockedResponse.body);
res.end();
} else {
next();
}
};
}
[/javascript]

We can now create more advanced mocks:

curl -X POST \
    -H "mock-method: DELETE" \
    -H "mock-response: 403" \
    -H "Content-type: application/json" \
    -H "mock-header-Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT" \
    -d '{"error": "Not authorized"}' https://xebia.com/blog:9000/mock/api/user

curl -D - -X DELETE https://xebia.com/blog:9000/api/user
HTTP/1.1 403 Forbidden
Content-Type: application/json
last-modified: Tue, 15 Nov 1994 12:45:26 GMT
Date: Wed, 18 Jun 2014 13:39:30 GMT
Connection: keep-alive
Transfer-Encoding: chunked
{"error": "Not authorized"}

Since we thought this would be useful for other developers, we decided to make all this available as open source library on GitHub and NPM
To add this to your project, just install with npm:

npm install mock-rest-request --save-dev

And of course add it to your middleware configuration:

[javascript]
middleware: function (connect, options) {
var mockRequests = require('mock-rest-request');
return [
mockRequests(),
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
[/javascript]
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts