Test Patterns In AngularJS

Agenda

  • About Me
  • Why?
  • Testing Types
  • Test Patterns & Examples

About Me

Name: Tomas Miliauskas

Focusing on: Client Side Development

Country: Lithuania

Team: Wix Tigers

Project: Wix Hotels

Wix Hotels

  • New Wix vertical
  • Internal TPA
  • Hybrid App
  • Multiwidget

Why?

Testing Types

  • Integration/e2e testing (protractor)

Testing an integration between components (scenarios).

  • Unit testing (karma)

Testing a particular piece of code (unit, function).

Test Patterns & Examples

Unit Testing

AngularJS Concepts

  • Module
  • Controller
  • Filter
  • Directive
  • Service

Testing Modules

Module is a container for the different parts of your application.

  • Using angular-mock to control DI
  • Using module() function
  • Usually we don't test modules
  • Mock dependencies (do it on purpose)
  • Only important part: config/run blocks
  • We use jasmine spies to mock.

Testing Modules (run/config)

describe('Back Office config & run phases', function () {
  var $rootScope = jasmine.createSpyObj('$rootScope', ['$on', '$watch']);
  var $location = jasmine.createSpyObj('$location', ['url']);
  var $translate = jasmine.createSpyObj('$translate', ['use']);
  var $wix = {};
  var clientConfig = {};
  var experiments = {};
  
  function createModel() {
    module('backOfficeApp', function ($provide) {
      $provide.constant('$rootScope', $rootScope);
      $provide.constant('$location', $location);
      $provide.constant('$translate', $translate);
      $provide.constant('$wix', $wix);
      $provide.constant('clientConfig', clientConfig);
      $provide.constant('experiments', experiments);
    });
    
    inject(); // We need to call this to force module to use provided mocks
  }
  
  afterEach(function () {
    $rootScope.$on.reset();
    $location.url.reset();
    $translate.use.reset();
    $wix = {};
    clientConfig = {};
    experiments = {};
  });
  
  describe('.run()', function () {
    describe('deep linking', function () {
      beforeEach(createModel);
    
      it('should add state change success event listener', function () {
        expect($rootScope.$on).toHaveBeenCalledWith('$stateChangeSuccess', jasmine.any(Function));
      });
  
      it('should take the location url', function () {
        $location.url.andReturn('/rooms/list');
        $rootScope.$on.mostRecentCall.args[1]();
        expect($location.url.callCount).toBe(1);
      });
  
      it('should push corerct state', function () {
        $location.url.andReturn('/ppicker');
        $wix.Dashboard = { pushState: jasmine.createSpy() };
        $rootScope.$on.mostRecentCall.args[1]();
        expect($wix.Dashboard.pushState).toHaveBeenCalledWith('#/ppicker');
      });
    });
  });
});

Testing Controllers

  • Using $controller service to test our controllers.
  • Passing dependencies through module, $controller service or injector.
  • Create a new controller before each test.
  • Don't forget to create and pass scope.
describe('Back Office: Controller', function () {
  describe('RatesCtrl', function () {
    var $rootScope, $controller;
    var $scope, showError, promise, rates;

    beforeEach(function () {
      showError = jasmine.createSpy('showError');
      promise = jasmine.createSpyObj('$promise', ['then']);
      rates = { $promise: promise, length: 0 };

      module('backOfficeApp', { showError: showError });

      inject(function ($injector) {
        $controller = $injector.get('$controller');
        $rootScope = $injector.get('$rootScope');
      });
      
      $scope = $rootScope.$new();
      $controller('RatesCtrl', {$scope: $scope, rates: rates});
    });

    it('should set rates', function () {
      expect($scope.rates).toBe(rates);
    });

    it('should set isEmpty', function () {
      expect($scope.isEmpty).toBe(false);
      promise.then.calls[0].args[0]();
      expect($scope.isEmpty).toBe(true);
    });

    it('should show error', function () {
      promise.then.calls[0].args[1]();
      expect(showError).toHaveBeenCalled();
    });
  });
});

Testing Controllers (example)

Testing Filters

describe('capitalize filter', function () {
  var capitalize;

  beforeEach(function () {
    module('hotel');
    inject(function ($filter) {
      capitalize = $filter('capitalize');
    });
  });

  it('returns empty string if falsy value is given', function () {
    expect(capitalize(false)).toBe('');
    expect(capitalize(0)).toBe('');
    expect(capitalize('')).toBe('');
    expect(capitalize(null)).toBe('');
    expect(capitalize(undefined)).toBe('');
  });

  it('capitalizes first word', function () {
    expect(capitalize('a few words')).toBe('A few words');
  });

  it('capitalizes each word', function () {
    expect(
        capitalize('What is love? Baby don\'t hurt me, don\'t hurt me no more.', true))
          .toBe('What Is Love? Baby Don\'t Hurt Me, Don\'t Hurt Me No More.');
  });
});

Using $filter service to test our filters.

Testing Directives

  • Using $compile service to test our directives.
  • Compile an actual HTML with a directive you want to test.
  • Need to trigger digest cycle manually.
  • Create a scope.
  • Spying on jQuery (Lite) element methods -  spyOn(element.fn, 'someMethod')
describe('wix-selected', function () {
  var $scope, $compile, $rootScope, $state;
  var html, elem;
  var element = angular.element;
 
  beforeEach(function () {
    module('backOfficeApp', {'globalInterceptor': {}});
  
    inject(function ($injector) {
      $compile = $injector.get('$compile');
      $rootScope = $injector.get('$rootScope');
      $state = $injector.get('$state');
    });
    $scope = $rootScope.$new();
  });

  function compile(attrs) {
    attrs = attrs || [];
    html = '<a ' + attrs.join(' ') + ' ui-sref="[\'base.hello.kitty\']" href=""></a>';
    elem = $compile(html)($scope);
    $scope.$digest();
  }
  
  it('should not initialiaze the directive if there is no ui-sref attribute', function () {
    spyOn($scope, '$on');
    compile();
    expect($scope.$on).not.toHaveBeenCalled();
  });
  
  it('should initialize when ui-sref attribute exists', function () {
    spyOn($scope, '$on');
    spyOn(element.fn, 'removeClass');
    compile(['wix-selected="[\'base.hello\']"']);
    expect($scope.$on).toHaveBeenCalledWith('$stateChangeSuccess', jasmine.any(Function));
    expect(element.fn.removeClass).toHaveBeenCalledWith('selected');
  });
  
  it('should add class ".selected" when state name matches', function () {
    $state.current.name = 'base.hello.kitty';
    spyOn(element.fn, 'addClass');
    compile(['wix-selected="[\'base.hello\']"']);
    expect(element.fn.addClass).toHaveBeenCalledWith('selected');
  });
  
  it('should add class ".selected" when at least 1 state name matches', function () {
    $state.current.name = 'base.hello.kitty';
    spyOn(element.fn, 'addClass');
    compile(['wix-selected="[\'base.bye\', \'base.hello\']"']);
    expect(element.fn.addClass).toHaveBeenCalledWith('selected');
  });
});

Testing Directives (example)

Testing Services

  • Service has five recipes: value, factory, service, provider and constant.
  • Testing is pretty the same.
  • Services have to be injected to test.
describe('refresher', function () {
  var $window;
  var storage, refresher, getTime;
  var items, siteId = 'simply-wondeful-site-id';
  var name = 'wix-booking-' + siteId;
  var verify = 'wix-booking-verify-' + siteId;
  
  beforeEach(function () {
    items = {};
    storage = jasmine.createSpyObj('storage', ['setItem', 'key', 'getItem']);
    storage.setItem.andCallFake(function (name, value) { items[name] = value; });
    storage.getItem.andCallFake(function (name) { return items[name]; });
    getTime = jasmine.createSpy().andReturn(1406851200000);
    $window = { localStorage: storage, XDate: { now: getTime } };
    
    module('hotel', function ($provide) {
      $provide.value('$window', $window);
      $provide.constant('siteConfig', { id: siteId });
    });
    
    inject(function ($injector) {
      refresher = $injector.get('refresher');
    });
  });
  
  it('should save initial time', function () {
    expect(getTime).toHaveBeenCalled();
  });
  
  it('should provide all necessary methods ', function () {
    expect(refresher.engage).toEqual(jasmine.any(Function));
    expect(refresher.check).toEqual(jasmine.any(Function));
  });
  
  describe('.engage()', function () {
    it('should store a timestamp', function () {
      getTime.andReturn(1406851200001);
      refresher.engage();
      expect(items[name]).toBe(1406851200001);
    });
  });
});

Testing Services (example)

Intergration Testing

Using Page Objects (1/2)

module.exports = (function () {

function HomePage() {
  
}

HomePage.prototype.open = function () {
  browser.get("/home");
};

HomePage.prototype.header = function () {
  return $('.header span');
};

return HomePage;

});

Using Page Objects (2/2)

var HomePage = require("./pages/home");

describe("scenario 1", function() {
  var page = new HomePage();
  
  describe("visit the home page", function() {
    it('should show header') {
      page.open();
      expect(page.header().isDisplayed()).toBeTruthy();
    }
  });
});

Questions?

Test Patterns In AngularJS

By Tomas Miliauskas

Test Patterns In AngularJS

  • 1,865