Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jasmine test a promise.then function

I try to test my app with Jasmine and got the following problem:
I will calculate something in the then function of my promise. That's the point where I need to test my code.

Here is the code of my controller:

  TestCtrl.$inject = ["$scope", "TestService"];
  /* ngInject */
  function TestCtrl($scope, TestService) {
    $scope.loadData = function () {
      TestService.getData().then(function (response) {
        $scope.data = response.data;
        $scope.filtered = $scope.data.filter(function(item){
          if(item.id > 1000){
            return true;
          }
          return false;
        })
      });
    }
  }

And my Jasmine test code:

describe('TestService tests', function () {
  var $q;
  beforeEach(function () {
    module('pilot.fw.user');
  });
  beforeEach(inject(function (_$q_) {
    $q = _$q_;
  }));
  describe('UserController Tests', function () {

    beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
      this.scope = $rootScope.$new();
      this.$rootscope = $rootScope;
      this.$httpBackend = _$httpBackend_;
      this.scope = $rootScope.$new();
      var TestServiceMock = {
        getData: function () {
          var deferred = $q.defer();
          var result = [{
            "id": 1720,
            "user": 1132
          },
            {
              "id": 720,
              "user": 132
            }, {
              "id": 1721,
              "user": 1132
            }];
          deferred.promise.data = result;
          deferred.resolve(result);
          return deferred.promise;
        }
      };
      this.controller = $controller('TestCtrl', {
        '$scope': this.scope,
        'TestService': TestServiceMock
      });
    }));

    it('test', function(){
      this.scope.loadData();
      expect(true).toBeTruthy();
    })
  });
});

The strange thing I don't understand is (tested with console logs):

  • My promise is created and returned
  • My loadData function is called and it will call the getData() function from the TestService
  • Everything inside the then function won't be executed although I return the promise as resolved

So how could I test the code inside the then function?
Thanks for help

like image 653
Jonas Hans Avatar asked Feb 16 '16 11:02

Jonas Hans


People also ask

How do I return a promise in Spyon?

Because your original method returns a promise, and your spy is also returning a promise (even an already resolved) you should use the then or the async await as you commented in the question. Using the done parameter from jasmine to notify this unit test is asynchronous and will complete when this callback is called.

How do you mock a function in Jasmine?

Using Jasmine spies to mock code Jasmine spies are easy to set up. You set the object and function you want to spy on, and that code won't be executed. In the code below, we have a MyApp module with a flag property and a setFlag() function exposed. We also have an instance of that module called myApp in the test.


4 Answers

the jasmine 'it' method takes a done parameter that you can call for async testing

it('Should be async', function(done) {
  someAsyncFunction().then(function(result) {
    expect(result).toBe(true);
    done();
  });
});

Feel free to go as deep as you want, just be sure to call done when EVERYTHING is finished. Jasmine's default timeout is 5 seconds per test, so if the async stuff isn't done by then jasmine will crash. You can change this setting in the configs or set it in the terminal.

This is straight from the jasmine docs, showing you how to handle the default timeout interval

describe("long asynchronous specs", function() {
  var originalTimeout;
  beforeEach(function() {
    originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
  });

  it("takes a long time", function(done) {
    setTimeout(function() {
      done();
    }, 9000);
  });

  afterEach(function() {
    jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
  });
});

I think that if it doesn't work in 10 seconds, you may have faulty methods. ESPECIALLY if you are talking to a local server / db. This stuff should only take this long if you are performing HEAVY computations, or are hitting an external api with a not-so-great internet connection. If everything is local (or stubbed / mocked!) then anything over 5-10 seconds is a definite red flag.

like image 94
Dustin Stiles Avatar answered Oct 08 '22 11:10

Dustin Stiles


You better watch this https://codecraft.tv/courses/angular/unit-testing/asynchronous/

You have actually 3 ways:

1) use regular it:

it('test', (done) => {
   const spy = spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
   spy.calls.mostRecent().returnValue.then(res => {
      ...your expect here...
      done();
   })
} );

2) use async in beforeEach and it:

it('test', async(() => {
    spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
    fixture.whenStable().then(res => {
       ...your expect here...
    })
 } ));

3) use fakeAsync if you don't have Http or XHR calls:

it('test', fakeAsync(() => {
    spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
    tick();
    ...your expect here...
 } ));
like image 26
Yevheniy Potupa Avatar answered Oct 08 '22 10:10

Yevheniy Potupa


hope this solution helps. One approach I've found useful when testing is mocking dependencies. I've tried to comment out what I've done as much as possible.

var returnMock, $scope, TestServiceMock, controller;

beforeEach(module('app'));

beforeEach(inject(function($controller) {
    returnMock = {
        then: jasmine.createSpy(),
    };
    $scope = {};
    // first assumption is You are testing TestService extensively,
    // I don't care about what getData has to do to get results
    // All I care about is it gets called when I call loadData
    TestServiceMock = {
        getData: jasmine.createSpy().and.returnValue(returnMock);
    };

    controller = $controller;
}));

it('should load data when loadData function is called and result set is 
under 1000', function() {
    controller('TestCtrl', {
        $scope,
        TestServiceMock
    });
    // another assumption is your data comes back in such a format
    // perhaps in the actual code check whether data exists and proceed
    // or do some other action
    var returnedData = {
        data: [
            {
                id: 1,
                name: 'item 1',
            },
        ]
    }
    // when I execute the function/method
    $scope.loadData();
    // I expect getData to be called
    expect(TestServiceMock.getData).toHaveBeenCalled();
    // I expect then to be called and the reason is I mocked it
    expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
    returnMock.then.calls.mostRecent().args[0](returnedData);
    // expect data on scope to be equal to my mocked data
    expect($scope.data).toEqual(returnedData.data);
    // don't expect any result because 1 < 1000
    expect($scope.filtered).toEqual([]);
    expect($scope.filtered.length).toEqual(0);
});

it('should load data when loadData function is called and result set is over 1000', 
     function() {
    controller('TestCtrl', {
        $scope,
        TestServiceMock
    });
    var returnedData = {
        data: [
            {
                id: 1,
                name: 'item 1',
            },
            {
                id: 1000,
                name: 'item 1000',
            },
            {
                id: 1001,
                name: 'item 1000',
            },
            {
                id: 1002,
                name: 'item 1002',
            }
        ]
    }
    $scope.loadData();
    expect(TestServiceMock.getData).toHaveBeenCalled();
    expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
    returnMock.then.calls.mostRecent().args[0](returnedData);
    expect($scope.data).toEqual(returnedData.data);
    // expect a result because some entries in the mocked data have id > 1000
    expect($scope.filtered).toEqual([
        {
            id: 1001,
            name: 'item 1000',
        },
        {
            id: 1002,
            name: 'item 1002',
        }]);
    expect($scope.filtered.length).toEqual(2);
});

Official Jasmine Docs explain most of the concepts extensively. Hope the solution helps!!!!

like image 43
mahad Avatar answered Oct 08 '22 12:10

mahad


Let me tell ya what I do, for Angular 1.x and 2.x+ projects. Use the angular testing tools to get rid of callbacks/nests in your async tests. In angular 1.x, that means using a combination of $q and $rootScope.$apply(). In angular 2.x+, that means using something like fakeAsync.

From the Angular 1.x docs

it('should simulate promise', inject(function($q, $rootScope) {
  var deferred = $q.defer();
  var promise = deferred.promise;
  var resolvedValue;

  promise.then(function(value) { resolvedValue = value; });
  expect(resolvedValue).toBeUndefined();

  // Simulate resolving of promise
  deferred.resolve(123);
  // Note that the 'then' function does not get called synchronously.
  // This is because we want the promise API to always be async, whether or not
  // it got called synchronously or asynchronously.
  expect(resolvedValue).toBeUndefined();

  // Propagate promise resolution to 'then' functions using $apply().
  $rootScope.$apply();
  expect(resolvedValue).toEqual(123);
}));

The disadvantage is that your code is tied to angular, the advantages are that your code is flat and it's portable to 2.x+!

I was a fan of the mocha test runner that allowed me to return promises in my tests, you could try to get that going, but there are downsides to that as well like needing to modify your code specifically for a test.

like image 34
Jacob McKay Avatar answered Oct 08 '22 11:10

Jacob McKay