Testing AngularJS applications

Tomasz Szewców

Agenda

  1. Tools
  2. Structuring tests
  3. Karma configuration
  4. Testing Controllers
  5. Testing Services
  6. Testing Directives
  7. E2E tests configuration
  8. Protractor locators

Exercises

  1. Test a controller
  2. Add a page object

Tools - unit testing

  • Karma - tool used to spawn a web server which loads your application's source code and executes your tests.
  • Jasmine - BDD framework for JavaScript, provides functions to help with structuring tests and making assertions.

https://docs.angularjs.org/guide/unit-testing

Tools - e2e testing

  • Selenium WebDriver - compact Object Oriented API, which wraps user interactions into methods that can be used to drive the browsers.
  • Protractor - end to end test framework for AngularJS applications built on top of WebDriver.

https://docs.angularjs.org/guide/e2e-testing

Structuring tests


          describe('A suite', function() {
      it('contains spec with an expectation', function() {
        expect(true).toBe(true);
      });
});
      

Karma configuration


// install Karma
npm install karma
		  

// install plugins
npm install karma-jasmine
npm install karma-phantomjs-launcher
npm install karma-chrome-launcher
		  

// run Karma
./node_modules/karma/bin/karma start / init / run
// alternative
npm install -g karma-cli
karma start / init / run
		  

http://karma-runner.github.io/0.13/intro/installation.html

Karma configuration file


// can be created with karma init command
module.exports = function (config) {
	config.set({
		basePath: '',
		frameworks: [],
		files: [],
	hostname: 'localhost',
		port: 9876,
		autoWatch: false,
		browsers: [],
		singleRun: false,
	})
};
// for debugging in a browser:
// - set single run to true
// - select other browser
		  

http://karma-runner.github.io/0.8/config/configuration-file.html

Testing a controller


describe('SampleCntl tests', function() {
	'use strict';

	var $scope;

	beforeEach(module('someModule'));

	beforeEach(inject(function($controller, $rootScope){
		$scope = $rootScope.$new();
		$controller('SampleCntl', {$scope: $scope});
	}));

	describe('some suite', function() {
		it('some spec', function() {
			// given 
			// when 
			$scope.someFunction();
			// then
		});
	});
});
		  

Testing a controller - alternative


describe('SampleCntl tests', function() {
	'use strict';

	var cntl;

	beforeEach(module('someModule'));

	beforeEach(inject(function($controller){
		cntl = $controller('SampleCntl', {});
	}));

	describe('some suite', function() {
		it('some spec', function() {
			// given 
			// when 
			cntl.someFunction();
			// then
		});
	});
});
		  

Exercise - test a controller


	  // 1. create a sample angular module
angular.module('sampleModule', []);

// 2. create a calculator controller with 2 functions
angular.module('sampleModule').controller('CalculatorCntl', function(){
	'use strict';

	this.factorial = function(n){};
	this.divide = function(a, b){};
});

// 3. specify appropriate files in the karma config file

// 4. implement controller's functionality using TDD
		  

Testing controller with mocks


		// sample controller code
angular.module('someModule').controller('SomeCntl', function($location){
	'use strict';
	
	this.goToDialog = function(path){
		$location.path(path);
	};
});

// test code

var cntl, locationMock = {
	path: angular.noop
};
			
beforeEach(inject(function($controller){
	// injection of mocked $location service
	cntl = $controller('SomeCntl', {$location: locationMock});
}));
		  

Testing a service


	  describe('data service tests', function () {
	  'use strict';

	  var someDataService;
	  
	  beforeEach(module('app'));

	  beforeEach(inject(function (_someDataService_) {
		someDataService = _someDataService_;
	  }));

	  describe('get data method', function () {
		  it('should return data', function () {
			  // given
			  var data = [];
			  // when
			  data = someDataService.getData();
			  // then
			  expect(data.length).toEqual(10);
		  });
	  });
});
		  

Testing service with mocks


// sample service code
angular.module('someModule').factory('serviceUnderTests', function('otherService'){
	'use strict';
	var data = [];
	
	return {
		getData: function(){
			angular.copy(otherService.getData(), data);
		},
		getCurrent: function(){
			return data;
		}
	};
});

// test code
var otherServiceMock = {getData: function(){return [1,2,3]}};
var serviceUnderTests;

beforeEach(function(){
	module('someModule');
	
	module(function($provide){
		// injecting other service with $provide service
		$provide.value('otherService', otherServiceMock);
	);
});
beforeEach(function(_serviceUnderTests_){
	serviceUnderTests = _serviceUnderTests_;
});
		  

Testing with $httpBackend


var booksData, $httpBackend;

beforeEach(inject(function (_booksData_, _$httpBackend_) {
	booksData = _booksData_;
	$httpBackend = _$httpBackend_;
}));

afterEach(function () {
	// then
	$httpBackend.verifyNoOutstandingExpectation();
	$httpBackend.verifyNoOutstandingRequest();
});

it('should load books', function () {
	// given
	var searchParams = {title: 'title', author: 'author'}, books = [], response = [
		{id: 0, title: 'title1'},
		{id: 1, title: 'title2'}
	];
	$httpBackend.expectGET('/books-management/books-list/books.json?author=author&title=title').respond(response);
	// when
	booksData.getBooks(searchParams).then(function (response) {
	books = response.data;
	});
	$httpBackend.flush();
	// then
	expect(books).toEqual(response);
	});
		  

Testing a directive


          describe('testing directive', function() {
          'use strict';

          var $compile, $rootScope;

          beforeEach(module('moduleName'));
          beforeEach(inject(function(_$compile_, _$rootScope_){
              $compile = _$compile_;
              $rootScope = _$rootScope_;
          }));

          it('should replace the directive with an appropriate content', function() {
              // given when
              var element = $compile('')($rootScope);
              $rootScope.$digest();
              // then
              expect(element.html()).toContain('expected content');
          });
});
      

E2E tests configuration


				// install protractor globally with the node package manager
npm install -g protractor
		  

		  // download webdriver 
webdriver-manager update
		  

		  // start selenium server
webdriver-manager start
		  

			// prepare the configuration file
exports.config = {
	seleniumAddress: 'http://localhost:4444/wd/hub',
	specs: ['todo-spec.js']
};
			

			// run e2e tests
protractor [name-of-config-file]
			

Configuration file options


			// running tests in other browsers
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec.js'],
  capabilities: {
	browserName: 'firefox'
  }
}

// running tests in many browsers
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec.js'],
  multiCapabilities: [{
	browserName: 'firefox'
  }, {
	browserName: 'chrome'
  }]
}
			

https://github.com/angular/protractor/blob/master/docs/referenceConf.js

Protractor locators


		// by binding
element(by.binding('item.name'));

// by model
element(by.model('item.name'));

// by css
element(by.css('some-css'));

// shorthand for css selectors
$('my-css') // the same as element(by.css('my-css'))

// by button text
element(by.buttonText('buttonText'));

// by tag name
element(by.tagName('tag-name'));

// by repeater
element.all(by.repeater('repeater'));
        

https://angular.github.io/protractor/#/api?view=ProtractorBy

Actions


		var el = element(locator);

// click on the element
el.click();

// send keys to the element (usually an input)
el.sendKeys('my text');

// clear the text in an element (usually an input)
el.clear();

// get the value of an attribute, for example, get the value of an input
el.getAttribute('value');
        

https://angular.github.io/protractor/#/api?view=ElementFinder

Page Objects

  • The methods represent the services that the page offers
  • Try not to expose the internals of the page
  • Generally don't make assertions
  • Methods return other PageObjects
  • Need not represent an entire page
  • Different results for the same action are modelled as different methods

https://code.google.com/p/selenium/wiki/PageObjects

Debugging


// set breakpoint
browser.pause();
      

// continue to the next step
c
      

// enter interactive mode
repl
      

// exit debugging
ctrl + C
      

Exercise - create a table list PO


      // 1. create a page object for tables list dialog
// - page object should have nextPage and getNumOfTables function
// - nextPage has to click on the next page button (use for example by.css selector)
// - getNumOfTables has to retrieve number of rows (use for example by.repeater selector)	  

// 2. use page object in a test together with signIn page object
// - sign in to table management
// - assert number of rows on the first page
// - move to the second page
// - assert number of tables on the second page