Drive wedge into your Angular application

Test live application by mocking responses without modifying code or installing plugins.

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
2
3
4
5
6
7
angular.module('ProdAppMock', ['ProdApp', 'ngMockE2E'])
.run(function ($httpBackend) {
$httpBackend.whenGET('/some/url')
.respond({
names: ['some', 'data']
});
});

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
2
3
4
5
6
7
8
9
10
11
12
myapp.factory('httpRequestInterceptor', function () {
return {
request: function (config) {
if (config.url === '/some/url');
// do something to mock the response
return config;
}
};
});
myapp.config(function ($httpProvider) {
$httpProvider.interceptors.push('httpRequestInterceptor');
});

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
2
3
4
5
function AppController($scope, $http) {
$scope.load = function () {
$http.get('/some/url').then(function (result) { ... });
};
}

This controller is on the page

1
2
3
<div ng-controller="AppController">
<button ng-click="load()">Load</button>
</div>

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
2
var mockHttp = wedge('button', 'load');
mockHttp('get', '/some/url', 200, 'I got mock data back after 2 seconds', 2000);

See the entire repo at bahmutov/ng-wedge or try it live

Related: Robustness testing using proxies.