Reputation: 43491
My test is:
(function() {
describe('Login Controller', function() {
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
var $controller, $httpBackend, $q, userServiceMock;
self.$rootScope = $injector.get('$rootScope');
self.$scope = $rootScope.$new();
self.$state = $injector.get('$state');
$q = $injector.get('$q');
$httpBackend = $injector.get('$httpBackend');
$controller = $injector.get('$controller');
userServiceMock = {
login: function(auth) {
self.deferred = $q.defer();
console.log("HERE!");
return self.deferred.promise;
}
};
self.createController = function() {
return $controller('LoginController', {
'$scope': self.$scope,
'$rootScope': self.$rootScope,
'$state': self.$state,
'userService': userServiceMock
});
};
return $httpBackend.whenPOST('http://localhost:9001/api/v1/session/check').respond({
authenticated: true
});
}));
it('should set the page title to "Login"', function() {
self.createController();
$scope.init();
expect($rootScope.pageTitle).toBe('Login');
return expect($scope.auth).toEqual({});
});
return it('should properly authenticate a user', function() {
self.createController();
$scope.init();
$scope.auth = {
username: '[email protected]',
password: 'mypassword'
};
$scope.login();
self.deferred.resolve({
authenticated: true
});
$scope.$root.$digest();
console.log($state.current.name);
expect($state.current.name).toBe('dashboard');
});
});
})();
Simple enough, I suppose? My controller is:
myApp.controller('LoginController', [
'$scope', '$rootScope', '$state', 'userService', function($scope, $rootScope, $state, userService) {
$scope.init = function() {
$rootScope.pageTitle = 'Login';
return $scope.auth = {};
};
$scope.login = function() {
return userService.login($scope.auth).then(function(response) {
if (response.authenticated === true) {
return $state.transitionTo('dashboard');
} else {
return console.log('bad password man');
}
});
};
return $scope.init();
}
]);
What I get in my test is:
NFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Mac OS X)]: Connected on socket TRjrAWL8PHdQ-bKqkyzK with id 61166122
LOG: 'HERE!'
PhantomJS 1.9.7 (Mac OS X) - Login Controller:
PhantomJS 1.9.7 (Mac OS X) should set the page title to "Login" PASSED
PhantomJS 1.9.7 (Mac OS X) should properly authenticate a user FAILED
PhantomJS 1.9.7 (Mac OS X): Executed 2 of 2 (1 FAILED) (0.02 secs / 0.018 secs)
PhantomJS 1.9.7 (Mac OS X)
PhantomJS 1.9.7 (Mac OS X) Login Controller should properly authenticate a user FAILED
Error: Unexpected request: GET templates/dashboard.html
No more request expected
The reason this happens is because the state transitioned to dashboard
, which then tried to load the dashboard.html
template. I don't necessarily want to / need to test that. I just need to test that it got to the right state. How can I set this up properly?
I'm using the Angular UI Router instead of the built in $routeProvider
Upvotes: 2
Views: 826
Reputation: 35900
You can mock out your template file and thus avoid the HTTP error with $templateCache
:
$templateCache = $injector.get('$templateCache');
$templateCache.put('templates/dashboard.html','<div>blank or whatever</div>');
... another way to do it is with $httpBackend
...
BTW, instead of using $injector
all the time you can just add the injectables to your inject
function...
beforeEach(inject(function($rootScope, $state, $httpBackend, $templateCache) {
I often use the underscore syntax for additional flexibility:
beforeEach(inject(function(_$rootScope_, _$state_, _$httpBackend_, _$templateCache_) {
Upvotes: 4
Reputation: 8406
The short answer is that you need to tell $httpBackend to expect the request. Karma will throw an error if any request is sent and not caught by $httpBackend, as they should all be mocked in unit tests. It might look something like...
$httpBackend.expectGET('/templates/dashboard.html');
$scope.login();
$httpBackend.flush();
I say this is only the short answer because there are a few things going on here and it would be a bit of a disservice not to mention them. First, you are using $httpBackend to catch the authentication POST request, however you aren't actually sending the request, because you are mocking the userService. You don't need to do both. You can remove this code...
return $httpBackend.whenPOST('http://localhost:9001/api/v1/session/check').respond({
authenticated: true
});
since that request won't actually be sent by your mock user service. That code might be useful in your userService's unit tests though.
I would also make use of the beforeEach/afterEach hooks. There is no need to call self.createController in all of your tests when you can call it once in the beforeEach function and it will run before each test. This could get messy if you aren't careful.
Finally, testing that a view changes and a new page title is set is pushing the limits of what unit tests are for. Ideally, you would test routing and page changes in integration tests, if at all. The only thing you are really testing in that second test is that...
$state.transitionTo('dashboard')
does what its supposed to (updates the current $state to 'dashboard'). You should trust this works. UI-router has its own tests that prove this works. I would strive to make your unit tests as small as possible and only test the specific behavior of the individual components of your application in isolation. So instead of asking..
"When I call $scope.login(), does the userService's login function get called and send a POST request and change the state?"
I would simply ask...
"When I call $scope.login(), does the userService's login function get called?"
The rest is not the responsibility of your controller. Whether the request is sent is the responsibility of the userService (and should be tested there) and whether the state changes when you call $state.transitionTo is the scope of ui-router and is already tested there. Leave the high level tests that span multiple components for integration tests.
Upvotes: 6