How NOT to test your
application
Who the fork are we?!
Ofir Dagan
Just some dude @wix
Nadav Leshem
Another dude @wix
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 nadav
How NOT to test your angular application
- 2,178