Atticus Atticus - 5 months ago 21
Javascript Question

Testing AngularJS services that have dependencies to spy on

The trouble comes when creating the provider for the mock dependency, as it needs to use

$q
, which is another service within angular, and these cannot be accessed when setting up the provider.

Imagine that we have a Factory we want to test:

angular.module('myApp').factory('MyFactory', function (MyDependency, $q) {
return {
doSomething: function () {
var deferred = $q.defer();
MyDependency.doAction().then(function (response) {
deferred.resolve(response);
// other events
}, function (error) {
deferred.reject(error);
// other events
});

return deferred.promise;
}
}
});


And the following unit test:

describe('Service: MyFactory', function () {
var myDependency, myFactory;

beforeEach(module('myApp'));

// The problem is here, as $q cannot be instantiated
// when setting up providers, and our mock service we are
// creating as the dependency for MyFactory requires $q
beforeEach(module(function ($provide, $q) {
var promise = $q.defer().promise;
myDependency = jasmine.createSpyObj('MyDependency', ['open']);
myDependency.open.andReturn(promise);
$provide.value('MyDependency', {
doAction: myDependency.open
});
}));

beforeEach(inject(function (MyFactory) {
myFactory = MyFactory;
}));

describe('MyDependency.doAction should be called', function () {
myFactory.doSomething();
expect(myDependency.open).toHaveBeenCalled();
// expect other events
});
});


MyDependency
has a function,
open
, that we need to watch and override the method with a custom promise that we will control the data being resolved and rejected. We can easily create the mock dependency that will be injected into
MyFactory
, but how can we access other services, like
$q
during this phase?

The only reasonable solution I've come up with is to set up the provider like so, but it gives us much less control and more workarounds to handle success vs failure compared to promise.reject() promise.resolve()

beforeEach(module(function ($provide) {
myDependency = jasmine.createSpyObj('MyDependency', ['doAction']);
myDependency.doAction.andCallFake(function (){
return {
then: function (success, err){
success.call(this);
}
};
});
$provide.value('MyDependency', {
open: myDependency.open
});
}));

Answer

Assumptions

  • I know almost nothing about MyDependency service
  • It is promise
  • I can only test behaviour of resolve or reject

The most strange line is $provide.value('MyDependency', {});. This is proof of concept, any comments are welcome.

First revision of the implementation

(function() {
  angular.module('myApp', []).factory('MyFactory', function(MyDependency, $q) {
    return {
      doSomething: function() {
        var deferred = $q.defer();

        MyDependency.doAction().then(function(response) {
          deferred.resolve(response);
        }, function(error) {
          deferred.reject(error);
        });

        return deferred.promise;
      }
    };
  });
})();

describe('myApp', function() {

  var MyFactory, MyDependency = {},
    $q, scope;

  beforeEach(function() {
    module('myApp');
  });

  beforeEach(function() {
    module(function($provide) {
      $provide.value('MyDependency', {});
    });
  });

  beforeEach(inject(function(_MyFactory_, _$q_, $rootScope) {
    MyFactory = _MyFactory_;
    $q = _$q_;
    scope = $rootScope.$new();
  }));

  describe('MyDependency', function() {
    var MyDependencyDefer;

    beforeEach(inject(function(MyDependency, $q) {
      MyDependencyDefer = $q.defer();
      MyDependency.doAction = jasmine.createSpy('MyDependency.doAction').and.returnValue(MyDependencyDefer.promise);
    }));

    it('resolves doAction()', function() {
      var stubSuccess = 'mock data';
      var doSomethingDefer = MyFactory.doSomething();
      MyDependencyDefer.resolve(stubSuccess);

      doSomethingDefer.then(function(r) {
        expect(r).toBe(stubSuccess);
      });

      scope.$digest();
    });

    it('rejects doAction()', function() {
      var stubError = 'reason error';
      var doSomethingDefer = MyFactory.doSomething();
      MyDependencyDefer.reject(stubError);

      doSomethingDefer.catch(function(r) {
        expect(r).toBe(stubError);
      });

      scope.$digest();
    });
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Second revision of the implementation (after two years of development)

(() => {
  angular.module('myApp', []).factory('MyFactory', ['MyDependency', MyDependency => {
    return {
      doSomething: () => MyDependency.doAction()
    }
  }])
})()

describe('MyFactory.doSomething() returns MyDependency.doAction()', () => {
  'use strict';

  const promiseResponse = {
    succ: 'success',
    err: 'error'
  }
  let MyFactory, MyDependency = {},
    $q, scope

  beforeEach(module('myApp'));

  beforeEach(() => {
    module(['$provide', $provide => {
      $provide.value('MyDependency', {})
    }]);
  });

  beforeEach(inject(['MyFactory', '$q', '$rootScope', (_MyFactory_, _$q_, $rootScope) => {
    MyFactory = _MyFactory_
    $q = _$q_
    scope = $rootScope.$new()
  }]));

  describe('MyDependency.doAction() returns promise', () => {
    let MyDependencyDefer, doSomethingDefer

    beforeEach(inject(['MyDependency', '$q', (MyDependency, $q) => {
      MyDependencyDefer = $q.defer()
      MyDependency.doAction = jasmine.createSpy('MyDependency.doAction').and.returnValue(MyDependencyDefer.promise)
    }]))

    beforeEach(() => {
      doSomethingDefer = MyFactory.doSomething()
    })

    it('that can be resolved', done => {
      MyDependencyDefer.resolve(promiseResponse.succ)

      doSomethingDefer.then(r => {
        expect(r).toBe(promiseResponse.succ)
        done()
      });

      scope.$digest()
    });

    it('that can be rejected', done => {
      MyDependencyDefer.reject(promiseResponse.err)

      doSomethingDefer.catch(r => {
        expect(r).toBe(promiseResponse.err)
        done()
      })

      scope.$digest()
    })
  })
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Comments