How NOT to test your

application
Who the fork are we?!


Ofir Dagan
Some dude @wix
Nadav Leshem
Some dude not @wix anymore
twitter: @ofirdagan2
github: @ofirdagan
github: @nadav-dav

Introducing
CAT WARS

function WeaponInterface() {
this.fire = ... // returns promise
this.reload = ...
}
function Shotgun() {
var ammo = 2;
this.fire = function () {
var defer = $q.defer();
if (ammo > 1) {
ammo--;
$timeout(function () {
defer.resolve('successfully shot shotgun');
}, 10);
} else {
defer.reject('failed to shoot shotgun, no ammo.');
}
return defer.promise;
};
this.reload = function () {
ammo = 2;
};
}
function AnimalInterface() {
this.fireWeapon = ... // returns promise
}
Putting the cat
to the test!

it('should fire the weapon and' +
'return a success status', function () {
var spy = jasmine.createSpy();
cat.fireWeapon().then(spy);
$rootScope.$digest();
expect(spy)
.toHaveBeenCalledWith('Cat successfully shot a weapon');
});beforeEach(function () {
module('catWarsApp');
module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
defer.resolve('successfully shot a weapon');
return defer.promise;
};
}
});
});
beforeEach(inject(function (Cat, _$q_, WeaponMock) {
cat = new Cat(new WeaponMock());
$q = _$q_;
}));function Cat(weapon) {
this.fireWeapon = function () {
return weapon.fire().then(function (weaponSuccessStatus) {
return 'Cat ' + weaponSuccessStatus;
});
};
}
handling failure

it('should report back if failed to shoot', function () {
fireWeaponSuccessful = false;
var spy = jasmine.createSpy();
cat.fireWeapon().then(spy);
$rootScope.$digest();
expect(spy)
.toHaveBeenCalledWith('Cat failed to shoot weapon');
});module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
if (fireWeaponSuccessful) {
defer.resolve('successfully shot a weapon');
} else {
defer.reject('failed to shoot weapon');
}
return defer.promise;
};
}
})function Cat(weapon) {
this.fireWeapon = function () {
return weapon.fire().then(function (weaponSuccessStatus) {
return 'Cat ' + weaponSuccessStatus;
}, function (weaponFailedStatus) {
return 'Cat ' + weaponFailedStatus;
});
};
}
Player 2
has entered the game

function Dog(weapon) {
this.fireWeapon = function () {
return weapon.fire().then(function (weaponSuccessStatus) {
return 'Dog ' + weaponSuccessStatus;
}, function (weaponFailedStatus) {
return 'Dog ' + weaponFailedStatus;
});
};
}
beforeEach(function () {
module('catWarsApp');
module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
if (fireWeaponSuccessful) {
defer.resolve('successfully shot a weapon');
} else {
defer.reject('failed to shoot weapon');
}
return defer.promise;
};
}
});
});
beforeEach(inject(function (Dog, _$q_, _$rootScope_, WeaponMock) {
dog = new Dog(new WeaponMock());
$q = _$q_;
$rootScope = _$rootScope_;
}));Let's reflect..

The stubbed value, is far away from the assertion
it('should fire the weapon and' +
'return a success status', function () {
...
expect(spy)
.toHaveBeenCalledWith('Cat successfully shot a weapon');
});module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
defer.resolve('successfully shot a weapon');
return defer.promise;
};
});We repeat stubbing 'Weapon' in Dog.spec.js
beforeEach(function () {
module('catWarsApp');
module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
if (fireWeaponSuccessful) {
defer.resolve('successfully shot a weapon');
} else {
defer.reject('failed to shoot weapon');
}
return defer.promise;
};
}
});
});Stubbing get REALLY complicated VERY fast!
beforeEach(function () {
module('catWarsApp');
module({
WeaponMock: function () {
this.fire = function () {
var defer = $q.defer();
if (fireWeaponSuccessful) {
defer.resolve('successfully shot a weapon');
} else {
defer.reject('failed to shoot weapon');
}
return defer.promise;
};
}
});
});Repeating pattern of async testing
beforeEach(function () {
module({
mock: function () {
this.asyncFunc = function () {
var defer = $q.defer();
...
defer.resolve(...);
...
defer.reject(...);
...
return defer.promise;
};
}
});
});Introducing
TADA!
(Testing Angular Driven Applications)


angular.module('catWarsTestKit', ['tada'])
.service('weaponMock', function (tadaUtils) {
this.fire = tadaUtils.createAsyncFunc('fire');
this.reload = tadaUtils.createFunc('reload');
});Creating a test kit
using TADA!
it('should fire the weapon and' +
'return a success status', function () {
// given
var cat = new Cat(weaponMock);
// when
cat.fireWeapon().then(spy);
weaponMock.fire.returns('successfully shot a weapon');
// then
expect(spy)
.toHaveBeenCalledWith('Cat successfully shot a weapon');
});it('should fire the weapon and' +
'return a success status', function () {
// given
var cat = new Cat(weaponMock);
// when
cat.fireWeapon().then(spy);
weaponMock.fire.returns('successfully shot a weapon');
// then
expect(spy)
.toHaveBeenCalledWith('Cat successfully shot a weapon');
});Stubbed value is close to the assertion
No more repeating the '$q' pattern
it('should fire the weapon and' +
'return a success status', function () {
// given
var cat = new Cat(weaponMock);
// when
cat.fireWeapon().then(spy);
weaponMock.fire.returns('successfully shot a weapon');no $timeout.flush()
no $rootScope.$digest()
module('catWarsTestKit');
beforeEach(inject(function (_Cat_, _weaponMock_) {
weaponMock = _weaponMock_;
Cat = _Cat_;
spy = jasmine.createSpy();
}));And the testkit is VERY reusable!

mock.func.whenCalledWithArgs('foo').returns('bar');
mock.func.whenCalledWithArgs('boom').rejects();WAIT!
there's more!!!
Questions?

Links
TADA Lib
Cat wars (the example project demonstrated here)
How NOT to test your angular application
By ofird
How NOT to test your angular application
- 1,873
