2013-02-24 16 views
22

Sto usando il gelsomino di unit test un controller angularjs che imposta una variabile della portata per il risultato della chiamata di un metodo di servizio che restituisce un oggetto promessa:Angularjs non promette di essere risolti in unit test

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

all'interno il servizio:

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

Questo funziona bene nel contesto della mia applicazione angularjs, ma non funziona nel test di unità di gelsomino. Ho confermato che il callback "then" è in esecuzione nel test unitario, ma la promessa $ scope.myVar non viene mai impostata sul valore restituito del callback.

Il mio test di unità:

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

Inoltre, se cambio il controller a seguito del test di unità passa:

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

Perché non il valore del risultato di callback promessa di essere propagata a $ portata .myVar nel test dell'unità? Vedi il seguente jsfiddle per codice completo funzionante http://jsfiddle.net/s7PGg/5/

risposta

21

Immagino che la chiave di questo "mistero" sia il fatto che AngularJS risolverà automaticamente le promesse (e renderà i risultati) se quelle usate in una direttiva di interpolazione in un modello. Quello che voglio dire è che, dato questo controller:

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

e il modello:

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

AngularJS, al termine $ http chiamata, si "vedere" che una promessa è stata risolta e si ri-renderizzare template con i risultati risolti. Questo è ciò che è vagamente menzionato nel $q documentation:

$ promesse Q sono riconosciuti dal motore di template in angolare, che significa che in modelli che è possibile trattare le promesse collegate a un ambito come se fossero i valori risultanti .

Il codice in cui si verifica questa magia può essere visto here.

MA, questa "magia" si verifica solo quando è presente un modello (servizio $parse, per essere più precisi) in fase di riproduzione. Nel test dell'unità non è presente alcun modello, quindi la risoluzione promessa non viene propagata automaticamente.

Ora, devo dire che questa propagazione automatica di risoluzione/risultato è molto comoda, ma potrebbe essere fonte di confusione, come possiamo vedere da questa domanda. Questo è il motivo per cui preferisco per propagare in modo esplicito i risultati di risoluzione che avete fatto:

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

grande risposta, mi sembrava di aver perso quel po 'nella documentazione. – robbymurphy

+0

Se stai prendendo in giro il back-end come lo sono io, il risultato sarà un composito con una proprietà "dati" che contiene il contenuto della risposta effettiva. – Gepsens

+3

In Angular 1.2, le promesse non vengono più risolte automaticamente (AKA, unwrapped). – zhon

3

@ pkozlowski.opensource risposto il perché (grazie!), Ma non il modo di aggirare l'ostacolo in fase di test.

La soluzione che ho appena raggiunto è quella di verificare che HTTP venga chiamato nel servizio, quindi spiare i metodi di servizio nei test del controller e restituire valori effettivi anziché promesse.

Supponiamo di avere un servizio per l'utente che parla al nostro server:

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 
    }, 

    // ... 

    }; 
}); 

test che il servizio, non ci importa ciò che accade ai valori restituiti, solo che le chiamate HTTP sono state fatte in modo corretto.

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

Ora nei test del controller, non è necessario preoccuparsi di HTTP. Vogliamo solo vedere che il servizio utenti viene messo al lavoro.

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

Per verificarlo, spiamo il servizio Utente.

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'; 

Non c'è bisogno di risolvere le promesse. Spero che questo ti aiuti.

+0

Nell'esempio in cui stai testando il servizio, abbiamo dovuto aggiungere quanto segue prima di chiamare $ httpBackend.flush(): $ rootScope. $ Apply() – blaster

+2

@blaster ... quindi stai facendo qualcosa di sbagliato. I servizi dovrebbero essere testati al di fuori del contesto dei controller e dell'ambito. –

8

Ho avuto un problema simile e ho lasciato il controller assegnando $ scope.myVar direttamente alla promessa. Poi nel test, ho incatenato un'altra promessa che afferma il valore atteso della promessa quando viene risolta. Ho usato un metodo di supporto come questo:

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

Nota che a seconda della ordinazione di quando si chiama expectPromisedValue e quando la promessa si risolve con il codice sotto test, potrebbe essere necessario avviare manualmente una finale ciclo digest per eseguire questi metodi then(), senza che il test possa passare indipendentemente dal fatto che lo resolvedValue corrisponda o meno allo expectedValue.

Per essere sicuri, mettere il grilletto in una chiamata afterEach() in modo da non ricordare per ogni prova:

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