Check out myAngular article series with live demos and Facebook group Angular - Advanced Topics

Angular integration tests

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Sun May 17 2015

Most developers are already familiar with unit testing, but in this article I will describe a second category of automated tests – integration tests.

The difference between unit tests and integration tests is in the targeting of the code under test. Unit tests target an isolated “unit” of code by simulating the integration points – typically through some form of mocking. Integration tests on the other hand test multiple units in combination without mocking out the integration points. Where you draw the line between a pure unit test and an integration test depends on how strictly you follow the definition. From a purist perspective one might argue that “unit tests” often become “light” integration tests by letting the code under test reach beyond an isolated unit through lack of mocking. In practice though, most developers don't worry about this distinction when testing code.

My definition does not label these accidental “integration tests” as true integration tests. My view of an integration test is a test where I purposely define and test an entire scenario of multiple units in combination.

In the following example I will demonstrate how to test a series of nested components with shared state. My sample code includes a parent directive where a user can add an inputed number to a list by clicking a button. Nested within there are two directives, one for calculating the sum of all items, and a second directive for building a comma separated string from the items in the list.

In order to write testable code it's very important to free the code of any view concerns. As you can see the operations are triggered by button clicks in the UI, but through abstraction, the code under test is ignorant of how it's being invoked upstream. This separation is key in order to write flexible and targeted tests. Including and compiling markup as part of the tests would have worked too, but I view that as a much more cumbersome and inflexible approach. Limiting the test to pure function calls makes it much easier to test the code under different conditions. In this case, working with controller methods directly, lets us exercise the code much like the UI actions would.

Another popular category of testing is browser/web testing via Selenium or similar frameworks. These tests are very realistic since they actually automate a browser. However, these tests carry with them a lot of complexity in terms of infrastructure and tend to be very unstable. Timing issues seem to always plague these types of tests and you often get false negatives. The other pain point is performance. As your test suite grows, execution time may approach tens of hours. I am not suggesting that you replace all your web tests with integration tests, and I actually think the two approaches work well in tandem. Offloading some of the testing to integration tests may in many cases help with stability and performance. It's however crucial that your code is properly decoupled and designed with tests in mind to benefit from this approach. Otherwise it will be a struggle to realistically test your code.

I don't recommend doing exhausting testing via integration tests, but instead focus on testing the different components in a combined happy path scenario. More detailed testing should be done through focused unit testing of each individual component. You might even want to share some of the unit test code in a way that makes it reusable in your integration tests.

Code

Integration Tests

describe('IntegrationTest', function() { var scope; var $controller; beforeEach(module('app')); beforeEach(inject(function(_$rootScope_, _$controller_){ scope = _$rootScope_.$new(); $controller = _$controller_; })); it('should calculate the sum and separate items by commas before and after adding a number', function() { //Main controller var addDataController = $controller('addDataController', {$scope:scope},{statistics:{numbers:[10,20,30,40]}}); //Child controllers var sumController = $controller('sumController', {$scope:scope},{numbers:addDataController.statistics.numbers}); var csvController = $controller('csvController', {$scope:scope},{numbers:addDataController.statistics.numbers}); //Operate on the default set of numbers sumController.calaculateSum(); expect(sumController.sum).toBe(100); csvController.separate(); expect(csvController.csv).toBe('10,20,30,40'); //Add new number to list addDataController.currentNumber = 100; addDataController.addData(); //Test that newly added number is reflected in child controllers sumController.calaculateSum(); expect(sumController.sum).toBe(200); csvController.separate(); expect(csvController.csv).toBe('10,20,30,40,100'); }); });

JavaScript

angular.module('app',[]); angular.module('app').controller('mainController', function(){ this.statistics = {numbers:[10,20,30,40]}; }); angular.module('app').controller('addDataController', function(){ this.addData = function(){ this.statistics.numbers.push(this.currentNumber); }; }); angular.module('app').directive('addData', function(){ return{ templateUrl:'addData.html', controller:'addDataController', controllerAs:'addDataController', bindToController:true, scope:{statistics:'=',currentNumber:'@'} }; }); angular.module('app').controller('sumController', function($scope){ this.calaculateSum = function(){ this.sum = 0; for(var i = 0; i < this.numbers.length; i++){ this.sum += this.numbers[i]; } }; }); angular.module('app').directive('sum', function() { return{ template:'<div><button ng-click="sumController.calaculateSum()">Calculate Sum</button>{{sumController.sum}}</div>', controller:'sumController', controllerAs:'sumController', bindToController:true, scope:{numbers:'='} }; }); angular.module('app').controller('csvController', function($scope){ this.separate = function(){ this.csv = ''; for(var i = 0; i < this.numbers.length; i++){ this.csv += ',' + this.numbers[i]; } this.csv = this.csv.substring(1, this.csv.length); }; }); angular.module('app').directive('csv', function() { return{ template:'<div><button ng-click="csvController.separate()">Comma separate</button>{{csvController.csv}}</div>', controller:'csvController', controllerAs:'csvController', bindToController:true, scope:{numbers:'='} }; });

Html

<div> <input type="number" ng-model="addDataController.currentNumber" /> <button ng-click="addDataController.addData()">Add</button> <div ng-repeat="number in addDataController.statistics.numbers track by $index"> {{number}} </div> <sum numbers="addDataController.statistics.numbers"></sum> <csv numbers="addDataController.statistics.numbers"></csv> </div>

The code is written using controllerAs syntax which makes it really easy to simulate isolate scope and pass changes down the chain.

If you liked this article, share it with your friends on social media:

We also have a new Facebook group about advanced Angular topics.

I invite you to follow me on twitter