unit tutorial test run karma example and javascript angularjs jasmine karma-runner

javascript - tutorial - karma js



La devolución de llamada PromularJS Promise no se activó en la prueba JasmineJS (3)

Por lo que puedo ver, el problema de por qué su código no está funcionando es que no ha inyectado $ scope. La respuesta de Michiels funciona porque él inyecta $ rootScope y llama al ciclo de resumen. Sin embargo, su $ apply () es un nivel superior al invocar el ciclo de resumen para que funcione también ... PERO! solo si lo inyectas en el servicio mismo.

Pero creo que un servicio no crea un elemento $ scope, por lo que necesita inyectar $ rootScope en sí mismo; por lo que yo sé, solo los controladores le permiten inyectar $ scope como su trabajo para crear un $ scope. Pero esto es un poco especulativo, no estoy 100 por ciento seguro al respecto. Sin embargo, probaría con $ rootScope, ya que la aplicación tiene $ rootScope desde la creación de ng-app.

''use strict''; angular.module(''app.services'', []) .value(''fbsdk'', window.FB) .factory(''Facebook'', [''fbsdk'', ''$q'', ''$rootScope'' function (FB, $q, $rootScope) { //<---No $rootScope injection //If you want to use a child scope instead then --> var $scope = $rootScope.$new(); // Otherwise just use $rootScope FB.init({ appId: ''xxxxxxxxxxxxxxx'', cookie: false, status: false, xfbml: false }); function getUserLoginStatus ($scope) { //<--- Use of scope here, but use $rootScope instead var deferred = $q.defer(); // This is where the deferred promise is resolved. Notice that I call // `$scope.$apply()` at the end to let Angular know to trigger the // `then()` callback in the caller of `getUserLoginStatus()`. FB.getLoginStatus(function (response) { if (response.authResponse) { deferred.resolve(true); } else { deferred.resolve(false) } $scope.$apply(); // <-- Tell Angular to trigger `then()`. USE $rootScope instead! }); return deferred.promise; } return { getUserLoginStatus: getUserLoginStatus }; }]);

Introducción al problema

Estoy intentando probar un servicio AngularJS que envuelve el objeto Facebook Facebook SDK FB ; sin embargo, la prueba no funciona, y no he podido averiguar por qué. Además, el código de servicio funciona cuando lo ejecuto en un navegador en lugar de una prueba de unidad JasmineJS , ejecuto con el JasmineJS prueba Karma .

Estoy probando un método asincrónico usando promesas angulares a través del objeto $q . Tengo las pruebas configuradas para ejecutar de forma asincrónica utilizando los métodos de prueba asíncronas Jasmine 1.3.1 , pero la función waitsFor() nunca devuelve true (ver código de prueba a continuación), simplemente agota el tiempo después de 5 segundos . (Karma aún no incluye la API de prueba asíncrona Jasmine 2.0).

Creo que podría ser porque el método then() de la promesa nunca se desencadena (tengo un console.log() configurado para mostrar eso), aunque estoy llamando $scope.$apply() cuando el el método asíncrono regresa, para que Angular sepa que debe ejecutar un ciclo de resumen y desencadenar la devolución de llamada en then() ... pero podría estar equivocado.

Este es el resultado de error que proviene de ejecutar la prueba:

Chrome 32.0.1700 (Mac OS X 10.9.1) service Facebook should return false if user is not logged into Facebook FAILED timeout: timed out after 5000 msec waiting for something to happen Chrome 32.0.1700 (Mac OS X 10.9.1): Executed 6 of 6 (1 FAILED) (5.722 secs / 5.574 secs)

El código

Esta es mi prueba unitaria para el servicio ( vea los comentarios en línea que explican lo que he encontrado hasta ahora ):

''use strict''; describe(''service'', function () { beforeEach(module(''app.services'')); describe(''Facebook'', function () { it(''should return false if user is not logged into Facebook'', function () { // Provide a fake version of the Facebook JavaScript SDK `FB` object: module(function ($provide) { $provide.value(''fbsdk'', { getLoginStatus: function (callback) { return callback({}); }, init: function () {} }); }); var done = false; var userLoggedIn = false; runs(function () { inject(function (Facebook, $rootScope) { Facebook.getUserLoginStatus($rootScope) // This `then()` callback never runs, even after I call // `$scope.$apply()` in the service :( .then(function (data) { console.log("Found data!"); userLoggedIn = data; }) .finally(function () { console.log("Setting `done`..."); done = true; }); }); }); // This just times-out after 5 seconds because `done` is never // updated to `true` in the `then()` method above :( waitsFor(function () { return done; }); runs(function () { expect(userLoggedIn).toEqual(false); }); }); // it() }); // Facebook spec }); // Service module spec

Y este es mi servicio angular que está siendo probado ( ver comentarios en línea que explican lo que he encontrado hasta ahora ):

''use strict''; angular.module(''app.services'', []) .value(''fbsdk'', window.FB) .factory(''Facebook'', [''fbsdk'', ''$q'', function (FB, $q) { FB.init({ appId: ''xxxxxxxxxxxxxxx'', cookie: false, status: false, xfbml: false }); function getUserLoginStatus ($scope) { var deferred = $q.defer(); // This is where the deferred promise is resolved. Notice that I call // `$scope.$apply()` at the end to let Angular know to trigger the // `then()` callback in the caller of `getUserLoginStatus()`. FB.getLoginStatus(function (response) { if (response.authResponse) { deferred.resolve(true); } else { deferred.resolve(false) } $scope.$apply(); // <-- Tell Angular to trigger `then()`. }); return deferred.promise; } return { getUserLoginStatus: getUserLoginStatus }; }]);

Recursos

Aquí hay una lista de otros recursos que ya he examinado para tratar de resolver este problema.

  • $q

    Esto explica cómo usar las promesas en Angular, así como dar un ejemplo de cómo probar el código de la unidad que usa promesas (observe la explicación de por qué $scope.$apply() necesita ser llamado para desencadenar la devolución de llamada then() ) .

  • Ejemplos de prueba Async Jasmine

  • Respuestas de StackOverflow

    • Promise callback no llamado en Angular JS
      • respuesta 1
      • Respuesta 2
    • angularjs - promesa nunca resuelta en el controlador
    • AngularJS promete no disparar cuando se devuelve desde un servicio

Resumen

Tenga en cuenta que soy consciente de que ya existen otras bibliotecas angulares para el JavaScript SDK de Facebook , como las siguientes:

No estoy interesado en usarlos ahora, porque quería aprender cómo escribir un servicio angular yo mismo. Por lo tanto, mantenga las respuestas restringidas para ayudarme a solucionar los problemas en mi código , en lugar de sugerir que utilizo el de otra persona .

Entonces, dicho esto, ¿alguien sabe por qué mi prueba no funciona?


TL; DR

Llame $rootScope.$digest() desde su código de prueba y pasará:

it(''should return false if user is not logged into Facebook'', function () { ... var userLoggedIn; inject(function (Facebook, $rootScope) { Facebook.getUserLoginStatus($rootScope).then(function (data) { console.log("Found data!"); userLoggedIn = data; }); $rootScope.$digest(); // <-- This will resolve the promise created above expect(userLoggedIn).toEqual(false); }); });

Plunker here .

Nota: eliminé las llamadas run() y wait() porque no son necesarias aquí (no se realizan llamadas asincrónicas reales).

Larga explicación

getUserLoginStatus() es lo que está sucediendo: cuando llama a getUserLoginStatus() , internamente ejecuta FB.getLoginStatus() que a su vez ejecuta su devolución de llamada inmediatamente , como debería, ya que se ha burlado de hacer exactamente eso. Pero su llamada $scope.$apply() .then() está dentro de esa devolución de llamada, por lo que se ejecuta antes de la declaración .then() en la prueba. Y desde then() crea una nueva promesa, se requiere un nuevo resumen para que esa promesa se resuelva.

Creo que este problema no ocurre en el navegador debido a una de dos razones:

  1. FB.getLoginStatus() no invoca su devolución de llamada inmediatamente, por lo que cualquier llamada a then() se ejecuta primero; o
  2. Algo más en la aplicación desencadena un nuevo ciclo de resumen.

Entonces, para concluir, si crea una promesa dentro de una prueba, explícitamente o no, tendrá que desencadenar un ciclo de resumen en algún momento para que esa promesa se resuelva.


''use strict''; describe(''service: Facebook'', function () { var rootScope, fb; beforeEach(module(''app.services'')); // Inject $rootScope here... beforeEach(inject(function($rootScope, Facebook){ rootScope = $rootScope; fb = Facebook; })); // And run your apply here afterEach(function(){ rootScope.$apply(); }); it(''should return false if user is not logged into Facebook'', function () { // Provide a fake version of the Facebook JavaScript SDK `FB` object: module(function ($provide) { $provide.value(''fbsdk'', { getLoginStatus: function (callback) { return callback({}); }, init: function () {} }); }); fb.getUserLoginStatus($rootScope).then(function (data) { console.log("Found data!"); expect(data).toBeFalsy(); // user is not logged in }); }); }); // Service module spec

Esto debería hacer lo que estás buscando. Al utilizar beforeEach para configurar su rootScope y afterEach para ejecutar la aplicación, también puede ampliar fácilmente la prueba para que pueda agregar una prueba si el usuario ha iniciado sesión.