ti-enxame.com

Angularjs promete não ser resolvido em teste de unidade

Estou usando o jasmim para testar a unidade um controlador angularjs que define uma variável no escopo para o resultado de chamar um método de serviço que retorna um objeto de promessa:

var MyController = function($scope, service) {
    $scope.myVar = service.getStuff();
}

dentro do serviço:

function getStuff() {
    return $http.get( 'api/stuff' ).then( function ( httpResult ) {
        return httpResult.data;
    } );
}

Isso funciona bem no contexto do meu aplicativo angularjs, mas não funciona no teste de unidade jasmim. Confirmei que o retorno de chamada "then" está sendo executado no teste de unidade, mas a promessa $ scope.myVar nunca é definida como o valor de retorno do retorno de chamada.

Meu teste de unidade:

describe( 'My Controller', function () {
  var scope;
  var serviceMock;
  var controller;
  var httpBackend;

  beforeEach( inject( function ( $rootScope, $controller, $httpBackend, $http ) {
    scope = $rootScope.$new();
    httpBackend = $httpBackend;
    serviceMock = {
      stuffArray: [{
        FirstName: "Robby"
      }],

      getStuff: function () {
        return $http.get( 'api/stuff' ).then( function ( httpResult ) {
          return httpResult.data;
        } );
      }
    };
    $httpBackend.whenGET( 'api/stuff' ).respond( serviceMock.stuffArray );
    controller = $controller( MyController, {
      $scope: scope,
      service: serviceMock
    } );
  } ) );

  it( 'should set myVar to the resolved promise value',
    function () {
      httpBackend.flush();
      scope.$root.$digest();
      expect( scope.myVar[0].FirstName ).toEqual( "Robby" );
    } );
} );

Além disso, se eu mudar o controlador para o seguinte, o teste de unidade passa:

var MyController = function($scope, service) {
    service.getStuff().then(function(result) {
        $scope.myVar = result;
    });
}

Por que o valor do resultado da chamada de retorno da promessa não está sendo propagado para $ scope.myVar no teste de unidade? Veja o jsfiddle a seguir para obter o código de funcionamento completo http://jsfiddle.net/s7PGg/5/

21
robbymurphy

Eu acho que a chave para esse "mistério" é o fato de o AngularJS resolver automaticamente as promessas (e renderizar resultados) se forem usadas em uma diretiva de interpolação em um modelo. O que quero dizer é que, dado esse controlador:

MyCtrl = function($scope, $http) {
  $scope.promise = $http.get('myurl', {..});
}

e o modelo:

<span>{{promise}}</span>

O AngularJS, após a conclusão da chamada $ http, "verá" que uma promessa foi resolvida e renderizará novamente o modelo com os resultados resolvidos. Isto é o que é vagamente mencionado na $ q documentation :

As promessas $ q são reconhecidas pelo mecanismo de modelagem em angular, o que significa que, nos modelos, é possível tratar as promessas anexadas a um escopo como se fossem os valores resultantes.

O código onde essa mágica acontece pode ser visto aqui .

MAS, essa "mágica" acontece apenas quando há um modelo ($parse serviço, para ser mais preciso) em jogo. No seu teste de unidade, não há modelo envolvido; portanto, a resolução da promessa não é propagada automaticamente .

Agora, devo dizer que essa propagação automática de resolução/resultado é muito conveniente, mas pode ser confusa, como podemos ver nesta pergunta. É por isso que prefiro propagar explicitamente os resultados da resolução, como você fez:

var MyController = function($scope, service) {
    service.getStuff().then(function(result) {
        $scope.myVar = result;
    });
}
21
pkozlowski.opensource

Eu tive um problema semelhante e deixei meu controlador atribuindo $ scope.myVar diretamente à promessa. Em seguida, no teste, acorrentei a outra promessa que afirma o valor esperado da promessa quando ela é resolvida. Eu usei um método auxiliar como este:

var expectPromisedValue = function(promise, expectedValue) {
  promise.then(function(resolvedValue) {
    expect(resolvedValue).toEqual(expectedValue);
  });
}

Nota que, dependendo da ordem de quando você chama o waitPromisedValue e quando a promessa é resolvida pelo seu código em teste, pode ser necessário acionar manualmente um ciclo de resumo final para que seja executado () métodos a serem disparados - sem ele, seu teste pode ser aprovado, independentemente de o resolvedValue ser igual ao expectedValue ou não.

Para ser seguro, coloque o gatilho em uma chamada afterEach () para que você não precise se lembrar dele em todos os testes:

afterEach(inject(function($rootScope) {
  $rootScope.$apply();
}));
8
Kevin McCloskey

@ pkozlowski.opensource respondeu o porquê (OBRIGADO!), mas não como contornar isso nos testes.

A solução que acabei de chegar é testar se o HTTP está sendo chamado no serviço e, em seguida, espionar os métodos de serviço nos testes do controlador e retornar valores reais em vez de promessas.

Suponha que tenhamos um serviço de Usuário que fale com nosso servidor:

var services = angular.module('app.services', []);

services.factory('User', function ($q, $http) {

  function GET(path) {
    var defer = $q.defer();
    $http.get(path).success(function (data) {
      defer.resolve(data);
    }
    return defer.promise;
  }

  return {
    get: function (handle) {
      return GET('/api/' + handle);    // RETURNS A PROMISE
    },

    // ...

  };
});

Testando esse serviço, não nos importamos com o que acontece com os valores retornados, apenas com o fato de as chamadas HTTP terem sido feitas corretamente.

describe 'User service', ->
  User = undefined
  $httpBackend = undefined

  beforeEach module 'app.services'

  beforeEach inject ($injector) ->
    User = $injector.get 'User'
    $httpBackend = $injector.get '$httpBackend'

  afterEach ->
    $httpBackend.verifyNoOutstandingExpectation()
    $httpBackend.verifyNoOutstandingRequest()          

  it 'should get a user', ->
    $httpBackend.expectGET('/api/alice').respond { handle: 'alice' }
    User.get 'alice'
    $httpBackend.flush()    

Agora, em nossos testes de controlador, não há necessidade de se preocupar com HTTP. Queremos apenas ver que o serviço do usuário está sendo colocado em funcionamento.

angular.module('app.controllers')
  .controller('UserCtrl', function ($scope, $routeParams, User) {
    $scope.user = User.get($routeParams.handle);
  }); 

Para testar isso, espionamos o serviço do usuário.

describe 'UserCtrl', () ->

  User = undefined
  scope = undefined
  user = { handle: 'charlie', name: 'Charlie', email: '[email protected]' }

  beforeEach module 'app.controllers'

  beforeEach inject ($injector) ->
    # Spy on the user service
    User = $injector.get 'User'
    spyOn(User, 'get').andCallFake -> user

    # Other service dependencies
    $controller = $injector.get '$controller'
    $routeParams = $injector.get '$routeParams'
    $rootScope = $injector.get '$rootScope'
    scope = $rootScope.$new();

    # Set up the controller
    $routeParams.handle = user.handle
    UserCtrl = $controller 'UserCtrl', $scope: scope

  it 'should get the user by :handle', ->
    expect(User.get).toHaveBeenCalledWith 'charlie'
    expect(scope.user.handle).toBe 'charlie';

Não há necessidade de resolver as promessas. Espero que isto ajude.

3
Christian Smith