Testing with AngularJS

Overview

  • Tools
  • Unit Testing
  • Directive Testing
  • E2E Testing

AngularJS Test Pyramid

Tools

  • Test Runner: Karma
  • Test Framework: Jasmine
  • Headless Browser: PhantomJS

Alternatives

  • Framework: Mocha
  • Assertions: Chai
  • Mocking: Sinon

Unit Tests

  • Test your business logic in Services, Controllers and Filters
  • Perfect for Test Driven Development (TDD)
  • Principle: Testing in Isolation -> Dependencies are mocked

Unit Testing


      angular.module('app.mathService', [])
        .service('mathService', function() {
          this.add = function(a, b) {
            return a + b;
          };
        });

      describe('mathService', function () {
        var mathService;

        beforeEach(function () {
          module('app.mathService');

          inject(function (_mathService_) {
            mathService = _mathService_;
          });
        });

        describe('add()', function() {
          it('adds two numbers', function() {
            expect(mathService.add(2, 3)).toEqual(5);
          });
        });
      });
    

Mocking


      angular.module('app.itemService', [])
        .service('itemService', function(validatorService) {
          this.update = function(item) {
            if (validatorService.isValid(item)) {
              // Update item
              return true;
            } else {
              return false;
            }
          };
        });
  

Mocking


      describe('itemService', function () {
        var itemService, isValid;

        beforeEach(function () {
          module('app.itemService', function($provide) {
            $provide.constant('validatorService', {
              isValid: function() { return isValid; }
            });
          });

          inject(function (_itemService_) {
            itemService = _itemService_;
          });

          isValid = false;
        });
      });
  

Mocking


      // $provide.constant('itemService', {
      //   isValid: function() { return isValid; }
      // });

      describe('update()', function() {
        it('updates when validation is passed', function() {
          isValid = true;
          expect(itemService.update({})).toBeTruthy();
        });

        it('does not update when validation fails', function() {
          isValid = false;
          expect(itemService.update({})).toBeFalsy();
        });
      });
  

Spying


      angular.module('app.itemService', [])
        .service('itemService', function(validatorService) {
          this.update = function(item) {
            if (validatorService.isValid(item)) {
              // Update item
              return true;
            } else {
              return false;
            }
          };
        });
  

Spying



      describe('itemService', function () {
        var itemService, validatorService;

        beforeEach(function () {
          module('app.itemService');

          inject(function (_itemService_, _validatorService_) {
            itemService = _itemService_;
            validatorService = _validatorService_;
          });
        });
      });
  

Spying



      describe('update()', function() {
        var item = {};

        it('updates when validation is passed', function() {
          spyOn(validatorService, 'isValid').and.returnValue(true);
          expect(validatorService.isValid).toHaveBeenCalledWith(item);
          expect(itemService.update(item)).toBeTruthy();
        });

        it('does not update when validation fails', function() {
          spyOn(validatorService, 'isValid').and.returnValue(false);
          expect(validatorService.isValid).toHaveBeenCalledWith(item);
          expect(itemService.update(item)).toBeFalsy();
        });
      });
  

Directive Tests

  • A part of the app tested through the DOM
  • Test interaction with UI
  • Integration Test

Directive Testing


          angular.module('app.incrementer', []).
            .directive('incrementer', function() {
              return {
                restrict: 'E',
                scope: {
                  item: '='
                },
                template: '',
                link: function(scope) {
                  scope.increment = function() {
                    item.count = item.count + 1;
                  }
                }
              };
            });
      

Directive Testing


        describe('incrementer', function () {
          var element, scope, item = {};

          beforeEach(function () {
            item.count = 0;

            module('app.incrementer');

            inject(function ($compile, $rootScope) {
              var $scope = $rootScope.$new();
              $scope.item = item;
              template = '';

              element = angular.element(template);
              $compile(element)($scope);
              $scope.$digest();

              scope = element.isolateScope();
            });
          });
        });
      

Directive Testing


        it('calls increment on click', function () {
          spyOn(scope, 'increment');
          element.find('button').click();
          expect(scope.increment).toHaveBeenCalled();
        });

        describe('increment()', function() {
          it('increments the item\'s count property', function() {
            expect(item.count).toEqual(0);
            scope.increment();
            expect(item.count).toEqual(1);
          });
        });
      

E2E (Scenario) Tests

  • Integrated with Angular: Knows when the app is ready
  • Protractor: Based on Webdriver
  • Jasmine like syntax

E2E Test Example


        describe('E2E Test', function () {

          beforeEach(function () {
            browser().navigateTo('http://localhost/url/to/test/index.html');
          });

          it("should show a dialog", function () {
            element('.show-dialog').click();
            expect(element(".dialog").count()).toBe(1);
          });
        });
      

Thank you

created by
Gernot Höflechner
Andi Marek

http://www.small-improvements.com