Testing
AngularJS
Samuli Ulmanen
Web Developer, Rovio Entertainment Ltd.
@sulmanen
Disclaimer
... or why AngularJS isn't more talked of in Helsinki
'We have hit peak javascript'
'Start thinking for yourselves, ditch this s**t before it's too late and learn how to write some actual f***ing code'
'If you need handholding with factories and providers and service provider factories ...'
Ron Ashton
Why ?
test writing is like this
when I don't write tests
Are we making the right tradeoff?
manually checking that stuff works in staging,
freeze and deploy
vs.
complete test suite
vs.
partial test suite and manual verification
Motivation
ship stuff that works
minimize failure demand
but...
can I stay out of the debugger and save time as well?
Got test suite?
refactor with confidence
documentation that doesn't go out of date
BDD only
communicate to stakeholders what's done
test driving
'meh'
'go away'
'why are u pushing this stuff'
I wanna do it like in the movies
Test drive
focus in the right place
stay out of the debugger
code guaranteed testable
analysis and code ready for change
Test drive
stay out of the debugger
up front costs 'cause you have to learn
Acceptance test drive
ATDD is another tool in the box to facilitate goal oriented communication and get everybody on the same page. It is about defining in a non-ambiguous way what is desired.
Stephan Schwab
methods
... have history
IoC & Dependency Injection
aid in writing testable code
Test Doubles
isolate code under test
Methods...
TDD evolved to BDD
red, green, refactor
communicate
ATDD
implement the right thing
IOC
public static void main() {
Car car = new Car('red');
car.drive(200);
car.fillUp();
car.turnRight();
car.turnLeft();
}
class Simulator {
private Car car;
Container(Car car) {
this.car = car;
}
void run() {
car.drive(200);
car.fillUp();
...
IoC
1983
Hollywood principle
Richard E. Sweet
DependeNcy injection
IoC
implementation
with contextual lookup
... enable the use of Test Doubles
Test doubles
Spy
Stub which records on how it was called
Mock
specification of expected calls
... enable us to write concise non brittle tests
test doubles
Dummy
pass around but never used
Fake
actual replacement component
Stub
returns specific values for a test
more: mocks aren't stubs Martin Fowler
TDD
1960
'practice of test-first development, planning and writing tests before each micro-increment'
'To shorten the total development time, some formal test documents (such as for acceptance testing) have been developed in parallel (or shortly before) the software is ready for testing. '
NASA Project Mercury
to put a man in space
TDD: So gagarin made it first?
April 12 1961
test driving failed to ship?
TDD
1979
the art of software testing
Separate debugging from testing
Glenford J. Meyers
TDD
2003
TDD rediscovered
Kent Beck XP fame
red, green refactor iterations
keep programmer focus
save time keeping out of debugger and avoid waste
BDD
2006
BDD, an evolution from TDD
Dan North
'I kept coming across the same confusion and misunderstandings. Programmers wanted to know where to start, what to test and what not to test,
how much to test in one go, what to call their tests, and how to understand why a test fails' (http://dannorth.net/introducing-bdd/)
ATDD
1960
NASA project Mercury
1996
realistic examples as a single source of truth
WyCash+ Project
Ward Cunningham
2002
Specification by Example
Martin Fowler
Materials
Task runner
grunt
gulp
make
something else perhaps...
Test Framework
Test Runner
Object mocking
sinon.js has mocks
Protractor
angular-mocks.js
window.module
-
initialise module
-
mock objects
-
include ngMock
window.inject
-
initialise $injector
window.dump
-
stringify
$httpBackend
expect order and existence of requests
expectGET
respond to any request
whenGET
Fixtures
the hard part
or
hurdle
Gherkin
Given
When
Then
Reading Gherkin in jasmine
describe('function load', function() { it('exists', function() { expect(rvoShortyCtrl.load).toBeDefined(); // Given }); describe('gets list url list', function() { beforeEach(inject(function(rvoShortyAllUrl, rvoUrlListDummy) { $httpBackend.expectGET(rvoShortyAllUrl).respond(200, rvoUrlListDummy); rvoShortyCtrl.load(); // When $httpBackend.flush(); })); it('and has url list populated', function() { expect(rvoShortyCtrl.urls.length).toBeGreaterThan(0); // Then }); });
Controllers
beforeEach(module('schedulerUi'));
beforeEach(inject(function ($rootScope, $controller, _rvoJobsServiceStub_, _rvoAppsServiceStub_, rvoAddStepTypeMsg, _rvoSchedulesServiceStub_) {
scope = $rootScope.$new();
ctrl = $controller('rvoJobCtrl', {
$scope: scope,
RVO_SCHEDULER_JOB_RUNNING: 'RUNNING',
rvoJobsService: _rvoJobsServiceStub_,
rvoAppsService: _rvoAppsServiceStub_,
rvoSchedulesService: _rvoSchedulesServiceStub_
});
jobCtrl = scope.rvoJobCtrl;
serviceMock = _rvoJobsServiceStub_;
appsServiceMock = _rvoAppsServiceStub_;
schedulesServiceMock = _rvoSchedulesServiceStub_;
stepType = rvoAddStepTypeMsg;
}));
Filter fixture
describe('rvoNumberFormat', function() {
var filter;
beforeEach(module('rvoFilter'));
beforeEach(inject(function($filter) {
filter = $filter('rvoNumberFormat');
}));
it('should exist', function() {
expect(filter).toBeDefined();
});
it('should withstand undefined', function() {
expect(filter()).toBe('');
});
it('should format according to numeral.js', function() {
expect(filter(0.23001, '.00')).toBe('.23');
});
Services
Injecting test doubles was tricky
describe('JobsService', function () {
var jobsService, $httpBackend, timeout, mockJobs;
beforeEach(module('schedulerUi', function($provide) {
$provide.value('rvoSchedulesService', {
getAppSchedules: function () {
},
getAllApps: function() {
},
reload: function() {
}
});
}));
directives
describe('rvoSchedulerClusterConfig', function () {
var el, scope, parentScope, ctrl;
beforeEach(module('schedulerUi'));
beforeEach(inject(function ($rootScope, $compile) {
parentScope = $rootScope.$new();
parentScope.base = {
cluster: {}
};
parentScope.nodeTypes = ['one', 'two'];
parentScope.nodeCounts = [1, 2];
el = angular.element('<rvo-scheduler-cluster-config cluster="base.cluster" node-types="nodeTypes" node-counts="nodeCounts"></rvo-sheduler-cluster-config>');
$compile(el)(parentScope);
parentScope.$digest();
scope = el.isolateScope();
ctrl = scope.rvoSchedulerClusterConfig;
}));
Directives
Use a $templateCache preloader such as
grunt-angular-prangler
include templates.js
with test dependencies
to prevent xhr's to directive templates
Providers
avoid at all costs
but
if you have to
var stateProvider;
beforeEach(function () {
var fake = angular.module('module.test', []);
fake.config(function (module_stateProvider) {
stateProvider = module_stateProvider;
});
module('module', 'module.test');
inject(function () {});
});
it('exists', function () {
expect(stateProvider).toBeDefined();
});
Providers
include the provider
before
the config block
use one when
you need to test code inside config block
or
you need a service which needs config
before its wired to other components
Gotchas
there were many...
but
Phantom PhantomJS
instances were the strangest
Page objects
Abstraction between the DOM and your tests.
PO benefits
Tests are easier to read
Suite is less brittle
var ShortyHome = function() { var urlInput = element(by.model('rvoShortyCtrl.url')); var button = element(by.css('.btn.btn-success')); var byError = by.css('.rvo-shorty-error'); var byShortLink = by.binding('rvoShortyCtrl.shortUrl'); var title = element(by.css('h1')); this.enterUrl = function(url) { urlInput.sendKeys(url); }; this.shorten = function() { return button.click(); }; this.error = function() { return element(byError).getText(); }; this.linkText = function() { return element(byShortLink).getText(); }; this.gotoLink = function() { element(byShortLink).click(); }; this.title = function() { return title.getText(); }; }; module.exports = ShortyHome;
Example test
TestingAngularJS
By samuli
TestingAngularJS
- 1,303