jesusgonzalezrivera
jesusgonzalezrivera

Reputation: 385

Karma/Jasmine test of ionic application not injecting controller

I am adding some tests to an ionic application using Karma and Jasmine. In this moment, I am trying to make the tests for the angular login controller. However it appears not to be executing the inject sentences which creates the controller and mocks all external dependencies. Here is the code I am using:

login.controller.js

(function() {
  var app = angular.module("myApp");

  app.controller("LoginController", function($translate, $state, $ionicPopup, $localStorage, sessionService, userService) {
    var vm = this;

    vm.email = null;
    vm.password = null;

    vm.login = function() {
      $localStorage.email = vm.email;
      $localStorage.password = vm.password;

      sessionService.login().then(
        function(data) {
          if (userService.isAuthenticated()) {
            // User is logged, go to the next state
            $state.go("next_state");
          } else {
            // User is not logged (invalid credentials), show an alert message
            var alertPopup = $ionicPopup.alert({
              title: $translate.instant("LOGIN_FAIL_TITLE"),
              template: $translate.instant("LOGIN_FAIL_MESSAGE")
            });
          }
        }
      );
    }
  });
})();

login.controller.tests.js

describe("LoginController", function() {
  var controller,
      deferredLogin,
      translateMock,
      stateMock,
      ionicPopupMock,
      localStorageMock,
      sessionServiceMock,
      userServiceMock;

  beforeEach(module("myApp"));

  beforeEach(module(function($provide, $urlRouterProvider) {
    $provide.value("$ionicTemplateCache", function() {});
    $urlRouterProvider.deferIntercept();
  }));

  beforeEach(inject(function($controller, $q) {
    deferredLogin = $q.defer();

    sessionServiceMock = {
      login: jasmine.createSpy("login spy")
                    .and.returnValue(deferredLogin.promise)
    };

    translateMock = jasmine.createSpyObj("$translate spy", ["instant"]);

    stateMock = jasmine.createSpyObj("$state spy", ["go"]);

    ionicPopupMock = jasmine.createSpyObj("$ionicPopup spy", ["alert"]);

    localStorageMock = jasmine.createSpyObj("$localStorage spy", ["getItem"]);

    userServiceMock = jasmine.createSpyObj("userService spy", ["isAuthenticated"]);

    controller = $controller("LoginController", {
      "$translate": translateMock,
      "$state": stateMock,
      "$ionicPopup": ionicPopupMock,
      "$localStorage": localStorageMock,
      "sessionService": sessionServiceMock,
      "userService": userServiceMock
    });
  }));

  describe("#login", function() {
    beforeEach(inject(function(_$rootScope_) {
      $rootScope = _$rootScope_;
      controller.email = "[email protected]";
      controller.password = "foobarfoo";
      controller.login();
    }));

    it("should call login on sessionService", function() {
      expect(sessionServiceMock.login).toHaveBeenCalledWith("[email protected]", "foobarfoo");
    });

    describe("when the login is executed", function() {
      it("if successful, should change state to next_state", function() {
        deferredLogin.resolve();
        $rootScope.$digest();
        expect(stateMock.go).toHaveBeenCalledWith("next_state");
      });

      it("if unsuccessful, should show a popup", function() {
        deferredLogin.reject();
        $rootScope.$digest();
        expect(ionicPopupMock.alert).toHaveBeenCalled();
      });
    });
  });
});

I have been searching over all places trying different solutions that I have found but I always obtain the same results:

...
PhantomJS 2.1.1 (Linux 0.0.0) LoginController #login should call login on sessionService FAILED
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    TypeError: undefined is not an object (evaluating 'sessionServiceMock.login') in unit-tests/controllers/login.controller.tests.js (line 63)
    unit-tests/controllers/login.controller.tests.js:63:32
    loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Linux 0.0.0) LoginController #login when the login is executed if successful, should change state to capture_image FAILED
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    TypeError: undefined is not an object (evaluating 'deferredLogin.resolve') in unit-tests/controllers/login.controller.tests.js (line 68)
    unit-tests/controllers/login.controller.tests.js:68:22
    loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Linux 0.0.0) LoginController #login when the login is executed if unsuccessful, should show a popup FAILED
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    forEach@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:13691:24
    loadModules@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17878:12
    createInjector@/home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17800:30
    workFn@/home/user/NSS/my-app/www/lib/angular-mocks/angular-mocks.js:2922:60
    loaded@http://localhost:9876/context.js:151:17
    /home/user/NSS/my-app/www/lib/ionic/js/ionic.bundle.js:17918:53
    TypeError: undefined is not an object (evaluating 'deferredLogin.reject') in unit-tests/controllers/login.controller.tests.js (line 74)
    unit-tests/controllers/login.controller.tests.js:74:22
    loaded@http://localhost:9876/context.js:151:17
...

I can't understand what I am doing wrong. Thanks in advance.

UPDATE:

As Matthew Green says, I have changed the way the tests are written. Now they follow his recommendations and I have also added some more injections which are necessary:

describe("LoginController", function() {
  var controller,
      stateMock,
      ionicPopupMock,
      localStorageMock,
      sessionServiceMockPromise,
      sessionServiceMock;

  // Load the application module
  beforeEach(module("myApp"));

  // Avoid trying to load all templates of the application
  beforeEach(module(function($provide, $urlRouterProvider) {
    $provide.value("$ionicTemplateCache", function() {});
    $urlRouterProvider.deferIntercept();
  }));

  // Avoid asynchronous loader of the translations (problem with the angular-translate-design)
  beforeEach(module(function($provide, $translateProvider) {
    $provide.factory("customLoader", function($q) {
      return function() {
        var deferred = $q.defer();
        deferred.resolve({});
        return deferred.promise;
      };
    });

    $translateProvider.useLoader("customLoader");
  }));

  // Instanciate and initialize the controller and mocks
  beforeEach(inject(function($controller, $q) {
    sessionServiceMockPromise = $q.defer();
    sessionServiceMock = jasmine.createSpyObj("sessionServiceMock", ["login"]);
    sessionServiceMock.login.and.callFake(function() {
      return sessionServiceMockPromise.promise;
    });

    stateMock = jasmine.createSpyObj("$state spy", ["go"]);

    ionicPopupMock = jasmine.createSpyObj("$ionicPopup spy", ["alert"]);

    localStorageMock = jasmine.createSpyObj("$localStorage spy", ["getItem"]);

    controller = $controller("LoginController", {
      "$state": stateMock,
      "$ionicPopup": ionicPopupMock,
      "$localStorage": localStorageMock,
      "sessionService": sessionServiceMock
    });
  }));

  describe("#login", function() {
    beforeEach(inject(function(_$rootScope_) {
      $rootScope = _$rootScope_;
      controller.login();
      sessionServiceMockPromise.resolve();
    }));

    it("should call login on sessionService", function() {
      expect(sessionServiceMock.login).toHaveBeenCalledWith();
    });

    describe("when the login is executed", function() {
      it("if successfull, should change state to capture_image", inject(function(userService) {
        spyOn(userService, "isAuthenticated").and.callFake(function() { return true; });
        $rootScope.$digest();
        expect(stateMock.go).toHaveBeenCalledWith("nextState");
      }));

      it("should show a popup", inject(function(userService) {
        spyOn(userService, "isAuthenticated").and.callFake(function() { return false; });
        $rootScope.$digest();
        expect(ionicPopupMock.alert).toHaveBeenCalled();
      }));
    });
  });
});

Upvotes: 0

Views: 463

Answers (1)

Matthew Green
Matthew Green

Reputation: 10401

It looks like your sessionService.login() is a function that should return a promise. That being the case I see a few issues with what you have.

First, I don't know why you choose to set up this one service different from the rest. This could most likely be some of your issues. If you set it up the same as the others it would look something like this.

var sessionServiceMockPromise = $q.defer();
sessionServiceMock = jasmine.createSpyObj('sessionServiceMock', ['login']);
sessionServiceMock.login.and.callFake(function() {
    return sessionServiceMockPromise.promise;
});

In this we establish that login is function that returns a promise. With sessionServiceMockPromise you can now call sessionServiceMockPromise.resolve([data]) (or reject() for that matter) in your tests to see the result of your promise.

Second, your test says that it expects the login to be called with two variables. No where in your code for either the service or test do I see that this is function that accepts variables. A better test might just be that it is called or that the state is set to what you would expect.

Upvotes: 1

Related Questions