What happens if the server returns an error to your Angular app? Can you confirm the error is handled on the client side? What if the data is delayed by 5 seconds? Are there race conditions?
Trying to test every possible end to end situation is extremely time consuming. Instead, it would be nice to recreate an error condition on the client side on demand. If you suspect that occasional 500 response when requesting a particular resource goes nowhere, could you recreate it on the spot to see how the app handles it? Probably not.
What we need is an ability to return mock data for some ajax requests on demand to observe the results. I wrote one solution that does not require changing the application's source code or installing any browser plugins. In fact, it does NOT even require reloading the application! Instead it modifies the application's source code on the fly, using the power of the JavaScript's dynamic nature to wrap around specific methods. Before describing the solution, let me describe the approaches to mocking the ajax requests to the server that do NOT work in this situation.
Using ngMockE2E httpBackend
I described how to run an Angular application without a server
by mocking the back end using $httpBackend
class from ngMockE2E
module. You can play with this
approach yourself in this repo and see it live here.
Unfortunately, this approach requires your production application depend on ngMockE2E
module
and defining ajax call mocks inside a run
block at startup.
1 | angular.module('ProdAppMock', ['ProdApp', 'ngMockE2E']) |
I would NOT put mocking backend into my production code of course.
Using AngularJS http interceptors
We could mock specific http requests using Angular http interceptors.
For example, we could add a new interceptor on request
action and modify the config, and probably
repoint the request at specific mock end point
1 | myapp.factory('httpRequestInterceptor', function () { |
I have not investigated this solution for two reasons
- Returning the mock data by modifying the
config
object was not obvious to me. - In order to add / modify mock interceptors, the application would need to make
httpRequestInterceptor
factory configurable from the outside. This would leave huge security / complexity door open into my application for anyone to peek / control.
Using Chrome extensions
One can potentially implement a Chrome extension to intercept / modify ajax requests on the fly. I do not like this solution. First, it will be difficult, because the plugins run in a sandbox isolated from the main code for security. Second, I would not feel comfortable installing such plugin.
ng-wedge
Let me describe now a solution that works against a live Angular application without installing
anything. Instead of intercepting all ajax requests, we will overwrite a specific method on a scope
object that makes use of $http
service. The main idea is to substitute a fake $http object into
that specific method.
A typical controller injects Angular $http
service and attaches load
method to the $scope
object
1 | function AppController($scope, $http) { |
This controller is on the page
1 | <div ng-controller="AppController"> |
At runtime we can get to the scope object and inspect / modify its values (see more examples in Angular from Browser Console).
// from the browser's console
var scope = angular.element(document.querySelector('button')).scope();
scope.load(); // executes $scope.load method
Not only we can call method on the scope method, we can modify it!. For example, we can substitute our own function
to run in place of $scope.load
var scope = angular.element(document.querySelector('button')).scope();
scope.load = function () {
console.log('you clicked Load button');
}
// click on "Load" button now - see the console message
We need to wrap the existing method instead of overwriting it completely
var scope = angular.element(document.querySelector('button')).scope();
var _previousLoad = scope.load;
scope.load = function () {
console.log('loading data');
_previousLoad();
}
Now we need to wrap the existing method and make it use our fake $http object instead of the Angular $http object accessible through the lexical scope.
var scope = angular.element(document.querySelector('button')).scope();
var _previousLoad = scope.load;
var fakeHttp = {
get: function (url) {
if (url === '/some/url') {
return 'mock data';
}
}
};
scope.load = function () {
console.log('loading data');
// HMM, _previousLoad still will use its own $http object!!!
_previousLoad();
}
I described how to change function's lexical scope in the blog post Faking Lexical Scope.
The main idea is to NOT call the original function, instead recreate it using eval(fn.toString())
call. The recreated function then uses the location of the eval call as its lexical scope.
If we have a fake $http
object above eval()
call, then the fake $http object will be used.
var scope = angular.element(document.querySelector('button')).scope();
var _previousLoad = scope.load;
var fakeHttp = {
get: function (url) {
if (url === '/some/url') {
return 'mock data';
}
}
};
var $http = fakeHttp;
var recreatedFunction = eval('(' + _previousLoad.toString() + ')');
scope.load = function () {
console.log('loading mock data');
recreatedFunction();
}
Now the recreated function runs the original $scope.load
but unknowingly uses fakeHttp
object.
Mission accomplished!
Details
Not only we can use fake $http
object, we can even use the real one to pass calls through.
In fact we can use anything available inside the original controller using its own injector!
var el = angular.element(document.querySelector('button'));
var scope = el.scope();
var injector = el.injector();
var _previousLoad = scope.load;
var _$http = injector.get('$http');
var $q = injector.get('$q');
var fakeHttp = {
get: function (url) {
if (url === '/some/url') {
return 'mock data';
} else {
// pass through real $http service
return _$http.get(url);
}
}
};
var $http = fakeHttp;
var recreatedFunction = eval('(' + _previousLoad.toString() + ')');
scope.load = function () {
// loading mock data for /some/url or real data for anything else
recreatedFunction();
}
Conclusions
Being able to mock specific ajax calls without modifying the application allows me to do a lot of Exploratory testing. Whenever I suspect that the user does not see an error message for specific response, or the app freezes because there is a race condition I can simply drive a wedge into the app and slow down a a specific request and return mock value. Just copy / paste ng-wedge.js into the browser's console and configure it.
1 | var mockHttp = wedge('button', 'load'); |
See the entire repo at bahmutov/ng-wedge or try it live
Related: Robustness testing using proxies.