angularjs - Obteniendo "$ digest ya en progreso" en una prueba asíncrona con Jasmine 2.0
jasmine2.0 (3)
@deitch tiene razón, ese $httpBacked.flush()
desencadena un resumen. El problema es que cuando $httpBackend.verifyNoOutstandingExpectation();
se ejecuta después de it
se completa, también tiene un resumen. Así que aquí está la secuencia de eventos:
- llamas a
flush()
que desencadena un resumen - el
then()
se ejecuta -
done()
se ejecuta -
verifyNoOutstandingExpectation()
se ejecuta y desencadena un resumen, pero ya está en uno para que obtenga un error.
done()
sigue siendo importante ya que necesitamos saber que incluso se ejecutan las ''esperadas'' dentro de then()
. Si no se ejecuta, entonces es posible que ahora sepa que hubo fallas. La clave es asegurarse de que el resumen esté completo antes de disparar el done()
.
it(''does GET requests'', function(done) {
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
service.get(''/some/random/url'').then(function(result) {
expect(result.data).toEqual(''The response'');
setTimeout(done, 0); // run the done() after the current $digest is complete.
});
$httpBackend.flush();
});
Al poner done()
en un tiempo de espera, se ejecutará inmediatamente después de que se complete el resumen actual (). Esto asegurará que realmente se ejecuten todas las expects
que quería ejecutar.
Sé que llamar $digest
o $apply
manualmente durante un ciclo de resumen provocará un error de "$ digest ya en curso", pero no tengo idea de por qué lo estoy obteniendo aquí.
Esta es una prueba unitaria para un servicio que envuelve $http
, el servicio es lo suficientemente simple, solo evita hacer llamadas duplicadas al servidor y al mismo tiempo garantizar que el código que intenta hacer las llamadas obtenga los datos que esperaba.
angular.module(''services'')
.factory(''httpService'', [''$http'', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.then(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, ''POST'');
},
get: function(url, data) {
return send(url, data, ''GET'');
},
_delete: function(url, data) {
return send(url, data, ''DELETE'');
}
};
}]);
La prueba de unidad también es bastante sencilla, usa $httpBackend
para esperar la solicitud.
it(''does GET requests'', function(done) {
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
service.get(''/some/random/url'').then(function(result) {
expect(result.data).toEqual(''The response'');
done();
});
$httpBackend.flush();
});
Esto explota cuando sone as done()
se llama con un error "$ digest ya en progreso". No tengo idea de por qué. Puedo resolver esto al envolver done()
en un tiempo de espera como este
setTimeout(function() { done() }, 1);
Eso significa que done()
se pondrá en cola y se ejecutará después de que se complete el $ digest, pero mientras eso resuelve mi problema, quiero saber
- ¿Por qué Angular está en un ciclo de digestión en primer lugar?
- ¿Por qué la llamada
done()
desencadena este error?
Tuve la misma prueba corriendo verde con Jasmine 1.3, esto solo sucedió después de actualizar a Jasmine 2.0 y volver a escribir la prueba para usar la nueva sintaxis asíncrona.
Añadiendo a la respuesta de @ deitch. Para hacer que las pruebas sean más robustas, puede agregar un espía antes de su devolución de llamada. Esto debería garantizar que tu devolución de llamada realmente se llame.
it(''does GET requests'', function() {
var callback = jasmine.createSpy().and.callFake(function(result) {
expect(result.data).toEqual(''The response'');
});
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
service.get(''/some/random/url'').then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
});
$httpBacked.flush()
realmente comienza y completa un ciclo $digest()
. Pasé todo el día de ayer investigando en la fuente de ngResource y los burlas angulares para llegar al fondo de esto, y todavía no lo entiendo del todo.
Por lo que puedo decir, el objetivo de $httpBackend.flush()
es evitar por completo la estructura asincrona anterior. En otras palabras, la sintaxis de it(''should do something'',function(done){});
y $httpBackend.flush()
no funcionan bien juntos. El objetivo de .flush()
es pasar por las devoluciones de llamada asíncronas pendientes y luego regresar. Es como una gran envoltura completa de todas tus devoluciones de llamada asincrónicas.
Entonces, si entendí correctamente (y me funciona ahora), el método correcto sería eliminar el procesador done()
al usar $httpBackend.flush()
:
it(''does GET requests'', function() {
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
service.get(''/some/random/url'').then(function(result) {
expect(result.data).toEqual(''The response'');
});
$httpBackend.flush();
});
Si agrega instrucciones console.log, encontrará que todas las devoluciones de llamada ocurren constantemente durante el ciclo flush()
:
it(''does GET requests'', function() {
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
console.log("pre-get");
service.get(''/some/random/url'').then(function(result) {
console.log("async callback begin");
expect(result.data).toEqual(''The response'');
console.log("async callback end");
});
console.log("pre-flush");
$httpBackend.flush();
console.log("post-flush");
});
Entonces la salida será:
pre-obtener
pre-flush
inicio de llamada asíncrono
extremo de devolución de llamada asíncrona
post-flush
Cada vez. Si realmente quieres verlo, toma el alcance y mira el scope.$$phase
var scope;
beforeEach(function(){
inject(function($rootScope){
scope = $rootScope;
});
});
it(''does GET requests'', function() {
$httpBackend.expectGET(''/some/random/url'').respond(''The response'');
console.log("pre-get "+scope.$$phase);
service.get(''/some/random/url'').then(function(result) {
console.log("async callback begin "+scope.$$phase);
expect(result.data).toEqual(''The response'');
console.log("async callback end "+scope.$$phase);
});
console.log("pre-flush "+scope.$$phase);
$httpBackend.flush();
console.log("post-flush "+scope.$$phase);
});
Y verá la salida:
pre-get undefined
pre-flush undefined
la devolución de llamada asincrónica comienza $ digest
async callback end $ digest
post-flush undefined