I found myself on the need of isolating my FrontEnd AngularJS app from the backend so the app can continue working without Internet access or without access to the backend services. I did not want, though, to add anything too aggressive into the app, but a piece of code that can be attached or unattached very easily and without the app having to know about it at all, like an app extension.
To do so, I found myself installing angular-mocks, creating a file that requests the library, attaches the $httpBackend to the app on run time, uses $httpBackend to mock the responses with some json files and forces the requests to be synchronous.
Step 1. Installing angular-mocks
First of all, to get access to $httpBackend we need to install angular-mocks, a plugin already developed for automated end-to-end testing purposes but that we will use for manual testing here.
So with the console open and using bower:
bower install angular-mocks --save-dev
Now, I want to add this lib to the project but without annoying the real codes of the app, so I create a new file where I set up the requireJs config:
require.config({ paths: { 'angular-mocks': '../vendor/angular-mocks/angular-mocks' }, shim: { 'angular-mocks': { deps: ['angular'] } } });
Angular has been already defined in the main app file so there’s no need to do that again in here but I set up that angular-mocks requires angular.
Step 2. Including httpBackend service into the app
Now we need to tell our app it requires angular-mocks so we get access to httpBackend, unfortunately our app is being declared somewhere else and I don’t want to modify the app’s real dependencies, so we will sort out a solution which doesn’t require to add the dependency into the base code:
require(["angular", "angular-mocks", "myAppName"], function () { var mainApp = angular.module("myAppName"); mainApp.config(function($provide){ $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); }); });
By requiring the library “angular-mocks” we make sure the file is there before accessing our app and injecting the decorator using $provide. Angular apps can have multiple .config() methods defined so this config() here won’t cause our main one to stop working.
Now we have effectively injected httpBackend into the app and can make use of it, but before we do that, and stealing Michal Ostruszka’s idea, let’s take this a little bit further and tell our plugin to attach the decorator only when working in local:
require(["angular", "angular-mocks", "myAppName"], function () { if(document.URL.match(/local/)){ mockBackendCalls(); } function mockBackendCalls(){ var mainApp = angular.module("myAppName"); mainApp.config(function($provide){ $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); }); } });
You can of course set up whatever flag suits you, like a queryString param or a cookie’s value to activate this, it’s just a way of telling it not to mock the requests all the time. Alternatively you can just comment that line and the mocking won’t happen.
Step 3. Mocking the backend
Well we can now mock the backend, which could be something as easy as this:
$httpBackend.whenGET(/conversations/).respond(function(method, url, data, headers, params) { return $resource("public/data/conversations.json").get(); });
Unfortunately that won’t work as it’s an asynchronous request and we can’t just execute httpBackend.flush() as we don’t want to be so aggressive with the original code, instead, we will use the DOM’s XMLHttpRequest object to make a manual synchronous request:
$httpBackend.whenGET(/conversations/).respond(function(method, url, data) { return makeSynchronousRequest('GET', 'public/data/conversations.json'); }); function makeSynchronousRequest(method, url){ var request = new XMLHttpRequest(); request.open(method, url, false); request.send(null); return [request.status, request.response, {}]; }
Finally, and even though I’m mocking the backEnd, I do not want to mock absolutely all the requests, as I have part of the data in the FrontEnd already, to let those request pass through we can do soemthing like this:
$httpBackend.whenGET(/.html/).passThrough(); $httpBackend.whenGET(/.js/).passThrough(); $httpBackend.whenGET(/.css/).passThrough();
And all the statics will be handled as usual.
Final code
Just as a summary of how the file looks like:
require.config({ paths: { 'angular-mocks': '../vendor/angular-mocks/angular-mocks' }, shim: { 'angular-mocks': { deps: ['angular'] } } }); require(["angular", "angular-mocks", "myAppName"], function () { if(document.URL.match(/local/)){ mockBackendCalls(); } function mockBackendCalls(){ var mainApp = angular.module("myAppName"); mainApp.config(function($provide){ $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); }); mainApp.run(function($httpBackend, $resource){ // Passthroughs $httpBackend.whenGET(/.html/).passThrough(); $httpBackend.whenGET(/.js/).passThrough(); $httpBackend.whenGET(/.css/).passThrough(); $httpBackend.whenGET(/conversations/).respond(function(method, url, data) { return makeSynchronousRequest('GET', 'public/data/conversations.json'); }); $httpBackend.whenGET(/conversation.*2377276/).respond(function(method, url, data) { return makeSynchronousRequest('GET', 'public/data/conversation-2377276.json'); }); // default conv: $httpBackend.whenGET(/conversation/).respond(function(method, url, data) { return makeSynchronousRequest('GET', 'public/data/conversation-2377883.json'); }); }); function makeSynchronousRequest(method, url){ var request = new XMLHttpRequest(); request.open(method, url, false); request.send(null); return [request.status, request.response, {}]; } } });
You can of course add more endpoint mocks there as need in a very easy way. Generating the json files and placing them into the data folder is a manual thing you will have to do on your own, if the backend points are already there when you are doing this, you can just copy paste an example response and save it as json, if not, you probably need to check the spec. to make sure you write a good mock model to handle.