web-dev-qa-db-fra.com

Jasmine test une fonction promise.then

J'essaie de tester mon application avec Jasmine et j'ai le problème suivant:
Je vais calculer quelque chose dans la fonction then de ma promesse. C'est le point où je dois tester mon code. 

Voici le code de mon contrôleur:

  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;
        })
      });
    }
  }

Et mon code de test Jasmine:

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();
    })
  });
});

La chose étrange que je ne comprends pas est (testé avec les journaux de la console): 

  • Ma promesse est créée et retournée
  • Ma fonction loadData est appelée et appelle la fonction getData () à partir de TestService.
  • Tout ce qui se trouve à l'intérieur de la fonction d'alors ne sera pas exécuté bien que je renvoie la promesse comme résolue.

Alors, comment pourrais-je tester le code dans la fonctionthen?
Merci pour l'aide

27
Jonas Hans

la méthode jasmine 'it' prend un paramètre done que vous pouvez appeler pour des tests asynchrones

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

N'hésitez pas à aller aussi loin que vous le souhaitez, assurez-vous d'appeler lorsque tout est terminé. Le délai d'attente par défaut de Jasmine est de 5 secondes par test. Par conséquent, si les opérations asynchrones ne sont pas terminées, jasmine se bloque. Vous pouvez modifier ce paramètre dans les configurations ou le définir dans le terminal.

Ceci vient directement de la documentation jasmine, vous montrant comment gérer le délai d'expiration par défaut

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;
  });
});

Je pense que si cela ne fonctionne pas dans 10 secondes, vous pouvez avoir des méthodes défectueuses. Surtout si vous parlez à un serveur local/db. Cela ne devrait prendre que longtemps si vous effectuez des calculs LOURDS, ou si vous frappez une API externe avec une connexion Internet pas très bonne. Si tout est local (ou stubbed/mocked!) Alors tout ce qui dépasse 5-10 secondes est un drapeau rouge définitif.

36
Dustin Stiles

espérons que cette solution aide. Une approche que j'ai trouvée utile lors des tests consiste à se moquer des dépendances. J'ai essayé de commenter ce que j'ai fait autant que 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);
});

Jasmine Docs officiel explique la plupart des concepts en détail. J'espère que la solution aide !!!!

4
mahadjr

Laissez-moi vous dire ce que je fais, pour les projets Angular 1.x et 2.x +. Utilisez les outils de test angulaires pour vous débarrasser des rappels/nids dans vos tests asynchrones. Dans angular 1.x, cela signifie qu’il faut utiliser une combinaison de $ q et de $ rootScope. $ Apply (). Dans angular 2.x +, cela signifie utiliser quelque chose comme fakeAsync.

À partir des documents 1.x angulaires

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);
}));

L'inconvénient est que votre code est lié à angular, les avantages sont que votre code est plat et qu'il est portable à 2.x +!

J'étais un fan du coureur de test mocha qui m'avait permis de retourner des promesses dans mes tests. Vous pouvez essayer de le faire, mais il y a des inconvénients à cela aussi, comme devoir modifier votre code spécifiquement pour un test.

2
Jacob McKay

En ce qui concerne votre contrôleur, vous devriez "renvoyer" des valeurs comme ceci.

TestCtrl.$inject = ["$scope", "TestService"];
/* ngInject */
function TestCtrl($scope, TestService) {
  $scope.loadData = function () {
    // Return this call, since it will return a new promise
    // This is what let's you do $scope.loadData.then()
    return TestService.getData().then(function (response) {
      // What you return in here will be the first argument
      // of your then method, in the tests / any env
      // Ex. return 'foo'
      // will result in .then(result => result === 'foo') //=> true
      // return one of these, i suggest the data, go SRP!
      return $scope.data = response.data;

      // I would do this stuff in a separate function, but you
      // can return 'filtered' instead if you like.
      //
      // $scope.filtered = $scope.data.filter(function(item){
      //   if(item.id > 1000){
      //     return true;
      //   }
      //   return false;
      // });
    });
  }
}

Rappelez-vous qu'appeler quelque chose APRÈS «alors» ne signifie rien, les valeurs doivent être appelées À L'INTÉRIEUR «alors». Pas après ou avant. Mais à l'intérieur Comme Tom Green et ce pauvre orignal dans Freddy Got Fingered.

0
Dustin Stiles