We needed to create a suite of e2e tests for our web app, preferably without having to hit the database for obvious reasons.
So basically I wanted to have a solution that satisfies these requirements:
- Allows easy switch between real and test data, for example by specifying a URL parameter.
- Allow specifying a mock data source, and a way to manipulate that mock data after it was loaded from the source.
Angular has a fake $httpBackend implementation designed specifically for providing a mock data response when you’re using $http service.
It is fairly straightforward:
Say, we have this HTML:
<div ng-controller="userController"> <button data-ng-click="click()">Click</button> <ul> <li data-ng-repeat="user in users">{{user.name}}</li> </ul> </div>and this app:
var app = angular.module('app', ['ngMockE2E']); app.controller("userController", function ($scope, userService) { $scope.users = []; $scope.click = function () { userService.getUsers().then(function (data) { $scope.users = data.data; }); }; }); app.factory("userService", function ($http) { return { getUsers: function () { return $http.get("/api/values"); } } }); app.run(function ($httpBackend, $location) { var users = []; var counter = 1; if ($location.absUrl().indexOf("test=true") > -1) { $httpBackend.whenGET('/api/users').respond(function (method, url, data) { return [200, users, {}]; }); $httpBackend.whenPOST('/api/users').respond(function (method, url, data) { users.push({ name: "user " + ++counter }); return [201, users, {}]; }); } $httpBackend.whenGET(/.*/).passThrough(); $httpBackend.whenPOST(/.*/).passThrough(); });This will catch requests to the /api/users, and replace the data from service with mocked data – if the URL contains test=true. Otherwise, it will just let the request through. Great, eh?
However, there’s a catch: “when” methods will return either a data object, or a function that returns an array with HTTP response status, data, and headers object (empty in my response). There’s no way to return a promise, which means that I can’t say something like “return $http.get(‘/mockData/users.json’)”, and need to preload mock data in the app initialization phase. Given that the app we’re building passes a pretty significant amount of data to and from the backend, this is very.. meh.
So, no go.
Enter the HTTP interceptors.
They do what the name says – intercept HTTP calls, and can modify pretty much every aspect of that call: a URL request goes to, a response data on a way back, or headers. This means that they get executed when the call actually happens, and I can redirect the call from say “/api/users” to “mockData/users.json”, and apply some processing on the response data. Now we’re talking.
In its simplest form interceptor looks like this:
app.factory("interceptor", function () { return { "request": function (config) { console.log("Intercepted!"); return config; } }; }) .config(function ($httpProvider) { $httpProvider.interceptors.push("interceptor"); });This will output “Intercepted!” on every HTTP request, and pass the data through.
The “config” parameter is an HTTP config, described fairly well in http://docs.angularjs.org/api/ng.$http. Note that the interceptor has to return it, or create a new one and return that.
So, in the interceptor function, we can change the url the request goes to, or change the response data, or both if we want to.Let’s get a bit fancy.
I want to create an interceptor that I can trigger on and off based on some configurable value, and I want to be able to configure how requests/responses to different routes will get modified. Also, it would be nice to chain those interceptors.So, here we go.
var Tester; (function (Tester) { var DataMocker = (function () { function DataMocker(location, cookieStore) { var _this = this; this.location = location; this.cookieStore = cookieStore; this._transformers = []; this._triggers = []; this.request = function (config) { if (!_this._isTriggered()) return config; var nextCalled = false; var next = function () { nextCalled = true; }; var url = config.url; for (var i = 0, len = _this._transformers.length; i < len; i++) { if (!_this._transformers[i]) continue; var transformer = _this._transformers[i]; if (transformer.matches(url)) { transformer.transformFunc(config, _this.location, _this.cookieStore, next); if (!nextCalled) return config; nextCalled = false; } } return config; }; this.set = function (urlStringOrRegex, transformFunc) { if (!urlStringOrRegex) throw new Error("Missing url string/regex"); if (!transformFunc) throw new Error("Missing transform function"); if (typeof (urlStringOrRegex) != "string" && !(urlStringOrRegex instanceof (RegExp))) throw new Error("Invalid url, must be either a string or RegExp"); if (typeof (transformFunc) != "function") throw new Error("transformFunc argument must be a function."); _this._transformers.push(new TransformerItem(urlStringOrRegex, transformFunc)); }; this.addTrigger = function (trigger) { if (!trigger || typeof (trigger) != "function") throw new Error("Invalid parameter, must be a function"); _this._triggers.push(trigger); }; this._isTriggered = function () { if (_this._triggers.length == 0) return false; for (var i = 0, len = _this._triggers.length; i < len; i++) { if (_this._triggers[i] && _this._triggers[i](_this.location, _this.cookieStore)) return true; } return false; }; } return DataMocker; })(); Tester.DataMocker = DataMocker; var TransformerItem = (function () { function TransformerItem(urlOrRegex, transformFunc) { this.urlOrRegex = urlOrRegex; this.transformFunc = transformFunc; } TransformerItem.prototype.matches = function (url) { if (typeof (this.urlOrRegex) == "string") return this.urlOrRegex === url; return this.urlOrRegex.test(url); }; return TransformerItem; })(); })(Tester || (Tester = {}));and then don’t forget to make it a module:
angular.module("Tester", []) .service("DataMocker", ($location: ng.ILocationService, $cookieStore: ng.cookies.ICookieStoreService) => { return new Tester.DataMocker($location, $cookieStore); });and add it to the list of references in your app:
angular.module("app", ["Tester", "ngCookies"])So now, lets say, I want to intercept calls to ‘/users’ when url contains ‘test=true’, and I also want to be able to specify a user role in a url:
app.run(function (DataMocker) { // intercept calls to /users, and replace it with a mock data from data1.js, then call next transformer function in chain. DataMocker.set("/users", function (config, location, cookies, next) { config.url = "/public/scripts/data1.js"; next(); }); // check if we have a user specified in url, and if so - replace userRole in data. Stop there for this route. DataMocker.set("/users", function (config, location, cookies, next) { var role = /userRole=\w*/i.exec(location.absUrl()); if (role) { config.transformResponse.push(function (data) { data.userRole = role[0].toLowerCase().replace("userrole=", ""); return data; }); } }); // Trigger the interceptor when url contains "test=true" DataMocker.addTrigger(function (location, cookieStore) { return location.absUrl().indexOf("test=true") > -1; }); })Not too hard now, yeah?
Hi Evgeni,
The problem that I have is to to use one of the webdriver to write acceptance tests that can run as unit tests in local machine regardless of the data. The interceptor is very simple and easy to implement. Thank you so much for this blog, it helped.
This is my implementation:
1) created new website with the following name: http://whatever.com/mock
2) At route configuration step:
function routeConfigurator($routeProvider, routes, $httpProvider) {
if (document.URL.indexOf(‘/mock/’) > 0) {
$httpProvider.interceptors.push(“httpMockinterceptor”);
}
routes.forEach(function (r) { $routeProvider.when(r.url, r.config); });
$routeProvider.otherwise({ redirectTo: ‘/’ });
}
3) Register service (for every url that starts with ‘api’ redirect url to file):
angular.module(‘common’).factory(‘httpMockinterceptor’, httpMockinterceptor);
function httpMockinterceptor() {
return {
“request”: function (config) {
if (config.url.indexOf(‘/api/’) == 0) {
config.url = ‘/mockApiData’ + config.url + ‘.html’;
}
return config;
}
};
}
Thanks Again 🙂
Saad Savari
Hello, I don’t understand why you pass a ‘next’ to the transformFunc and execute it. Is it because if there are more transformers for this urlOrRegexp, the for loop would apply it furthermore?
this is exactly what i was trying to achieve. thank you.
did you put this on github?