Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit-test promise-based code in Angularjs

I'm having hard times trying to test promise-based code in Angularjs.

I have the following code in my controller:

    $scope.markAsDone = function(taskId) {         tasksService.removeAndGetNext(taskId).then(function(nextTask) {             goTo(nextTask);         })     };      function goTo(nextTask) {         $location.path(...);     } 

I'd like to unit-test the following cases:

  • when markAsDone is called it should call tasksService.removeAndGetNext
  • when tasksService.removeAndGetNext is done it should change location (invoke goTo)

It seems to me that there is no easy way to test those two cases separately.

What I did to test the first one was:

var noopPromise= {then: function() {}} spyOn(tasksService, 'removeAndGetNext').andReturn(noopPromise); 

Now to test the second case I need to create another fake promise that would be always resolved. It's all quite tedious and it's a lot of boilerplate code.

Is there any other way to test such things? Or does my design smell?

like image 808
Michal Ostruszka Avatar asked Apr 18 '13 11:04

Michal Ostruszka


People also ask

How do I run a unit test in AngularJS?

Load the Angular App beforeEach(module('MyApp')); //3. Describe the object by name describe('compute', function () { var compute; //4. Initialize the filter beforeEach(inject(function ($filter) { compute = $filter('compute', {}); })); //5. Write the test in the it block along with expectations.

Is AngularJS code unit testable?

AngularJS is written with testability in mind, but it still requires that you do the right thing. We tried to make the right thing easy, but if you ignore these guidelines you may end up with an untestable application.


2 Answers

You will still need to mock the services and return a promise, but you should use real promises instead, so you don't need to implement its functionality. Use beforeEach to create the already fulfilled promise and mock the service if you need it to ALWAYS be resolved.

var $rootScope;  beforeEach(inject(function(_$rootScope_, $q) {   $rootScope = _$rootScope_;    var deferred = $q.defer();   deferred.resolve('somevalue'); //  always resolved, you can do it from your spec    // jasmine 2.0   spyOn(tasksService, 'removeAndGetNext').and.returnValue(deferred.promise);     // jasmine 1.3   //spyOn(tasksService, 'removeAndGetNext').andReturn(deferred.promise);   })); 

If you'd rather prefer to resolve it in each it block with a different value, then you just expose the deferred to a local variable and resolve it in the spec.

Of course, you would keep your tests as they are, but here is some really simple spec to show you how it would work.

it ('should test receive the fulfilled promise', function() {   var result;    tasksService.removeAndGetNext().then(function(returnFromPromise) {     result = returnFromPromise;   });    $rootScope.$apply(); // promises are resolved/dispatched only on next $digest cycle   expect(result).toBe('somevalue'); }); 
like image 114
Caio Cunha Avatar answered Nov 04 '22 01:11

Caio Cunha


Another approach would be the following, taken straight out of a controller I was testing:

var create_mock_promise_resolves = function (data) {     return { then: function (resolve) { return resolve(data); }; };  var create_mock_promise_rejects = function (data) {     return { then: function (resolve, reject) { if (typeof reject === 'function') { return resolve(data); } }; };  var create_noop_promise = function (data) {     return { then: function () { return this; } }; }; 
like image 37
yangmillstheory Avatar answered Nov 03 '22 23:11

yangmillstheory