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

Cat wars (the example project demonstrated here)

https://github.com/nadav-dav/tada-example

How NOT to test your angular application

By nadav

How NOT to test your angular application

  • 2,178