Pregunta

myApp.run([
  '$rootScope', 'userService', function($rootScope, userService) {
    return userService.isAuthenticated().then(function(response) {
      if (response.data.authenticated) {
        return $rootScope.$broadcast('login', response.data);
      } else {
        return userService.logout();
      }
    });
  }
]);

That's my code that I have in an init.js file. How can I unit test this?

¿Fue útil?

Solución

Unit testing a run(..) block is as simple as loading your module(..) in jasmine.

All of the code below is available in this plunker.

Consider the following very simple app.js:

var _idx = 1;
window.log = function(s) {
  console.log('(' + _idx++ + ') ' + s);
};

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

app.run(function($rootScope) {
  log('run block');
  $rootScope.valueSetInRun = 666;

});

app.controller('MainCtrl', function($scope) {
  log('MainCtrl block');
});

Note that the markup is irrelevant in this app.

Now consider the following test:

describe('myapp', function() {

  beforeEach(module('myapp'));

  beforeEach(inject(function($rootScope) {
    log('beforeEach');
  }));

  it('should allow me to test the run() block', inject(function ($rootScope) {
    log('it block');
    expect( $rootScope.valueSetInRun ).toBe(666);
  }));
});

Here is the console output:

(1) run block 
(2) MainCtrl block 
(3) run block 
(4) beforeEach 
(5) it block 

Note that the test passes

Some important observations

  • The first two console outputs, (1) and (2) are logged during normal app execution
  • Log messages (3) to (5) are outputted while the test is running.

Given the above, we can conclude the following:

  1. In the unit test, the module 'myapp' was instantiated
  2. In the unit test, MainCtrl wasn't instantiated
  3. In the unit test, the run block was executed
  4. When running a unit test, the beforeEach...run block is executed first, then beforeEach and finally the it block.
  5. The second beforeEach is not useful for unit testing a run block (note that you can put a beforeEach block before beforeEach(module('myapp'));, however if you try creating an injector in that block your test will fail)

And finally, we can conclude that the code above is how you can test a run block. As to your particular test, armed with this knowledge, it will be possible to construct a unit test using dependency injection mocking and Jasmine's toHaveBeenCalled() assertion.

Otros consejos

One easy method would be to store an 'authentication' object in $rootScope. In your unit test, expect that value to be empty or undefined, then populate the object in your service, and re-test against the value or length of that object.

I haven't tested this yet, but I'm pretty sure it works this way. The app.run method is meant to be run as soon as the given module has loaded all of its dependencies, but before it initializes any controllers. In your test (assuming you're using jasmine), you call that initializer by doing something like:

describe('MyApp', function(){
    beforeEach(function(){
        module('MyApp');
    });
    ...
    // more test code goes here
    ...
});

That call to module('MyApp') is what initializes the module, which "should" (in theory because I haven't actually tested it yet) call the run method immediately thereafter. Which means that you should be able to test the run block by doing something slightly different:

describe('MyApp', function(){
    beforeEach(function(){
        // whatever you need
    });

    describe('.run()', function(){
        it('should check if user is authenticated', function(){
            // expect not to have checked
            module('MyApp'); // this should call the .run function
            // expect to have checked
        });
    });
});

I may be completely off, but I believe that should get you started. Hope it helps.

I had similar problem and decided to pull everything into an application level service. To me it felt a little messy and poorly organized - especially if everything is neat and tide the angular way. The run block felt a little like a dumping ground.

So as we have access to D.I. unlike the config block we can just stick all our code in a service and fire it through an init method.

Application Service

myApp.factory('Application', function($rootScope, userService) {

    function setAuthenticated(response) {
        if (response.data.authenticated) {
          return $rootScope.$broadcast('login', response.data);
        } else {
          return userService.logout();
        }
    }

    function authenticate() {
      userService.isAuthenticated()
        .then(setAuthenticated);
    }

    function init() {
      authenticate();
    }

    return {
      init: init
    }
  }
]);

Run Method

myApp.run(function(Application) {
  Application.init();
});

Our application service can now be in charge of all the stuff that needs to be done at the start of the app - its just a simple factory so we can unit test the shit outta it - and we can add to it as needed.

As a side note I prefer to let ngmin take care of my angular minification issues - leads to cleaner more readable code for the developer.

You could try mocking your $rootScope, userService like this:

  var fakeRootScope;
  var fakePromise;
  var fakeUserService;

  beforeEach(function() {
      fakeRootScope = {}; //a fake $rootScope so that we can mock the $broadcast function

     //a fake promise with a .then function which calls the provided callback
     //  passing in a fake response provided by our test cases.
      fakePromise = {
         response: null,
         then: function(callback) {
             callback(this.response);
         }
      };

      //mock the userService by $provide.factory assuming that you register 
      //your userService using .factory. If you use .service to register, 
      // you might need to use .service to create the mock
      //mock the $rootScope by $provide.value
      module('plunker', function($provide) {
         $provide.factory("userService", function() {

          fakeUserService = {
             isAuthenticated: function() {
                    return fakePromise;
             },
                  logout: function() {}
           };

           fakeUserService.logout = jasmine.createSpy("logout");

           return fakeUserService;
        });

       fakeRootScope.$broadcast = jasmine.createSpy("$broadcast");
       $provide.value('$rootScope', fakeRootScope);
     });
  });

In your tests, you could provide your fake authenticated to test 2 cases:

    it('Is Not authenticated', function() {
      fakePromise.response = {
        data: {
          authenticated: false //test login failing case
        }
      }

      inject(function(userService) {
         //verify that userSerivce.logout is called when login failed
          expect(userService.logout).toHaveBeenCalled();
      });
    });

    it('Is authenticated', function() {
      fakePromise.response = {
        data: {
          authenticated: true //test login succeeding case
        }
      }

      inject(function($rootScope) {
 //verify that $rootScope.$broadcast is called with 2 parameters when login succeed 

 expect($rootScope.$broadcast).toHaveBeenCalledWith('login',fakePromise.response.data);
      });
    });

DEMO

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top