The marmelab blog

FakeRest: Patch XMLHttpRequest to fake a REST server based on JSON data

Published on 25 March 2015 by François with tags popular tdd oss rest

How do you manage E2E testing of webapps relying on RESTful web services? Instead of setting up a server with test data, why not do it directly in the browser? FakeRest allows to do backend-less e2e testing.

Lego House by Shadowman39, on Flickr

The Problem: e2e Testing a Webapp Depending on REST

While developing single-page applications relying on RESTful web services, end-to-end (E2E) testing is a challenging task. E2E tests require a test REST backend, accessible from the continuous integration server, with test data synchronized with the test cases.

At marmelab, we have met this particular problem with ng-admin, the Angular.js admin GUI pluggable to any RESTful API. We use Test-Driven-Development as much as possible, but E2E quickly became an issue with this project.

Bad Solution #1: json-server

We first chose to use json-server. It’s a simple Node.js app powering a complete REST API, and based only on a single JSON data file.

// example usage of json-server
var jsonServer = require('json-server')
var data = {
  posts: [
    { id: 1, body: 'foo' }
  ]
};

var server = jsonServer.create();
server.use(jsonServer.router(data));
server.listen(3000);

It’s convenient enough to have worked for several months, but lately the ng-admin CI failed more and more for bad reasons, tests and test data weren’t in sync, and the test build took longer due to HTTP transport.

Bad Solution #2: $httpBackend

So we decided to replace the stub REST server by a fake HTTP backend implementation on the client side. The idea is to patch the XMLHTTPRequest object in the test browser, and make it reply with canned responses when receiving predefined requests.

Angular.js comes with $httpBackend, and we also use sinon.js, a popular stubbing framework, which comes with a similar feature.

// example usage of Angular.js' $httpBackend
beforeEach(inject(function($injector) {
    // Set up the mock http service responses
    $httpBackend = $injector.get('$httpBackend');
    // backend definition common for all tests
    authRequestHandler = $httpBackend
        .when('GET', '/posts')
        .respond([{ id: 1, body: 'foo' }], {'A-Token': 'xxx'});
}));

However, both $httpBackend and sinon.fakeServer provide very low-level primitives: they allow to mock the response for a single request. We needed to easily mock an entire REST web server, based on test data. Listing all the mock responses for every e2e scenario soon became a nightmare.

The Good Solution: FakeRest

What we actually needed is a fake REST server built on top of a fake HTTP backend. During last week’s Hack Day at marmelab, I decided to give it a try. I chose to build it on top of sinon.js rather than $httpBackend, to allow the tool to be used with or without Angular.js. As for everything we build, I tried to follow the best practices of web development: ES6 code transpiled to JavaScript with Babel, unit tests running in the browser with Karma, compilation and minification done with WebPack, installation via bower or npm. It took a little longer than a day, but here is it: it’s called FakeRest, and we decided to open-source it.

<script src="/path/to/FakeRest.min.js"></script>
<script src="/path/to/sinon.js"></script>
<script type="text/javascript">
// initialize fake REST server and data
var restServer = new FakeRest.Server();
restServer.init({
    'authors': [
        { id: 0, first_name: 'Leo', last_name: 'Tolstoi' },
        { id: 1, first_name: 'Jane', last_name: 'Austen' }
    ],
    'books': [
        { id: 0, author_id: 0, title: 'Anna Karenina' },
        { id: 1, author_id: 0, title: 'War and Peace' },
        { id: 2, author_id: 1, title: 'Pride and Prejudice' },
        { id: 3, author_id: 1, title: 'Sense and Sensibility' }
    ]
});
// use sinon.js to monkey-patch XmlHttpRequest
var server = sinon.fakeServer.create();
server.respondWith(restServer.getHandler());

// Further requests will be answered by the fake REST server
var req = new XMLHttpRequest();
req.open("GET", "/authors", false);
req.send(null);
console.log(req.responseText);
// [
//    {"id":0,"first_name":"Leo","last_name":"Tolstoi"},
//    {"id":1,"first_name":"Jane","last_name":"Austen"}
// ]

var req = new XMLHttpRequest();
req.open("GET", "/books/3", false);
req.send(null);
console.log(req.responseText);
// {"id":3,"author_id":1,"title":"Sense and Sensibility"}

var req = new XMLHttpRequest();
req.open("POST", "/books", false);
req.send(JSON.stringify({ author_id: 1, title: 'Emma' }));
console.log(req.responseText);
// {"author_id":1,"title":"Emma","id":4}

// restore native XHR constructor
server.restore();
</script>

FakeRest is fully configurable (API endpoint, REST flavor, interceptors, logging level, etc.). You will find all the usage details in the FakeRest README file.

The ultimate test for FakeRest is to run a complete webapp without any backend. That’s what you will find in the example/ directory: the ng-admin demo (posts, comments, tags) running with no REST server at all.

Conclusion

FakeRest can be used with Angular.js, React.js, Amber.js, and any other framework you choose for your single-page application. It should simplify the e2e test stack of sophisticated webapps, and speed up the test builds by saving the HTTP transport time.

We hope that you’ll find it useful. If you find a bug or wish to add a feature, don’t hesitate to open a GitHub issue on the FakeRest repository.

comments powered by Disqus