Feb
13

Faster AngularJS tests using the component driver pattern

posted on 13 February 2018 in programming

Warning: Please consider that this post is over 5 years old and the content may no longer be relevant.

When writing tests I prefer to test multiple functions and their interactions at once, rather than constraining a test to a single function or unit. This ensures that the test operates in more realistic way, and avoids maintaining unnecessary tests over code paths that can’t be reached.

Protractor e2e tests for AngularJS interact directly with the DOM and usually provide more value than unit tests. I’m not interested in whether a controller behaves a certain way, I’m more interested in what happens when a user clicks a certain button on a page. But protractor tests are slow. In the largest AngularJS project I manage, there are over 300 protractor tests that take up to 15 minutes to complete, that’s a very slow feedback loop. In order to bring the test time down I’m turning back to AngularJS unit tests, using the component driver pattern to still inspect and interact with DOM elements.

The component driver test pattern

Based on the techniques described in Testing angular components — you don’t know what you’re missing, the component driver pattern reorganises test code so that rendering an AngularJS component and inspecting or interacting with it is abstracted from the test function into a component driver class, making it much easier to read and follow.

The component driver class should contain a render function which uses AngularJS’s $compile to convert a template into a jqLite / jQuery element. Other functions of the driver are written specifically for interacting with this component, e.g. getFirstName, setDescription, clickNextButton. Please excuse my ES5 here, you should use the class syntax where available.

var TodoItemDriver = function($compile) {
    this.$compile = $compile;
};

TodoItemDriver.prototype = {
    render: function(scope) {
        this.element = angular.element('<todo-item todo="todo"></todo-item>');
        this.element = this.$compile(this.element)(scope);
        scope.$digest();
    },

    getTitle: function() {
        return this.element.find('label').text();
    }
}

The test code then gets reduced to creating the driver, setting any scope variables to pass to the component, rendering the component and inspecting it.

beforeEach(inject(function($rootScope, _$compile_) {
    $scope = $rootScope.$new();
    component = new TodoItemDriver(_$compile_);

    $scope.todo = {
        uid: '102c2049-4a69-49d7-9395-24189e99a141',
        title: '  test todo ',
    };
}));

it('should display the trimmed Todo title', function () {
    component.render($scope);
    expect(component.getTitle()).to.equal('test todo');
});

CSS styles

One thing to consider with this type of testing is that CSS styles are going to be incomplete as we are only rendering a small part of the DOM. Prefer to test that an element has a class, e.g. ‘ng-hide’, rather than that a specific style has been applied.

Stateless components

A stateless component is one whose rendered output is only dependant on its input bindings / scope, this means it doesn’t take service dependencies to fetch state from localStorage or APIs. Obviously some area of the system needs to manage state, but it’s good practice to write your components to be stateless if possible, which makes them much easier to test. All you should need to test is that given a certain scope, the rendered output / element is as expected.

Vertical testing

As much as possible I encourage testing multiple areas of the system and their interactions. Don’t test the view, the controller and the service in isolation, instead interact with the view and check that the service responds accordingly. Only mock out the edges of your application (e.g. API calls, localStore, etc…). This style of testing is sometimes called Subcutaneous Testing, or for a less squeamish sounding name, Vertical Testing.

Vertical testing can reduce the number of tests and the amount of test maintenance required. You don’t really care whether a view uses a particular controller right? You care whether the correct operations occur when something interacts with the system.

In closing…

There are still situations where isolated unit tests make sense, and also where Protractor tests are more appropriate (expecially when testing interactions between multiple components from different areas of the DOM), but the majority of scenarios should be tested using the component driver pattern to get the best balance of performance and maintainability.

A reference project is available at https://github.com/phdesign/todo-better-angular