I am trying to write unit tests for the AngularJS application. Below is a pretty standard test template, which works fine:
describe('Controller: MainCtrl', function () {
var MainCtrl, scope;
beforeEach(function() { // <-- what if I remove this
module('watcomApp');
inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope,
});
});
}); // <-- and this
it('some spec', function () {
expect(scope.data).toEqual('something');
});
});
However if I try to reuse the current state of controller and remove beforeEach:
describe('Controller: MainCtrl', function () {
var MainCtrl, scope;
module('watcomApp');
inject(function ($controller, $rootScope) {
...
it stops working because scope.data becomes undefined.
The question is: what happens to scope? I expect it to persist between specs since it's global.
Related
I am working to initiate a controller sits inside a directive. I have some tests I need to run but right now I am not able to access the controller with ng-Mock.
describe('hero Directive', function () {
var $compile,
$rootScope,
$scope,
element,
ctrl;
beforeEach(function () {
angular.mock.module('ha.module.core');
angular.mock.inject(function (_$compile_, _$rootScope_, _$controller_, $templateCache) {
$compile = _$compile_;
element = angular.element("<div exlore-hereo></div");
$compile(element)($rootScope);
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
ctrl = _$controller_('ExploreHeroController', { $scope: $scope });
console.log(ctrl)
$scope.$digest();
});
});
afterEach(function () {
// need to remove the element element.remove();
});
describe('directive controller', function () {
it('should dispatch call $emit with $methodsBound', function () {
//spyOn($scope, '$emit');
spyOn($scope, 'ControllerName');
//expect(scope.$emit).toHaveBeenCalledWith('$methodsBound');
//expect(ctrl).toHaveBeenCalled();
});
});
});
I created an element compiled it and called the $digest method.
The error that I got was
Argument 'scope' is required.
So I tried spying on the it with jasmine
spyON($scope, 'ControllerName');
My controller inside of my directive is pretty basic.
var ControllerName = function($scope) {
$scope.$emit('$method');
}
It seems like I need a spy, but I am not sure why the one I created does not work.
You can try to spy on $scope, but note that ControllerName is not a member of the $scope object.
However, $emit is....
The thing is, that you call $emit in the controllers constructos, therefore you have to spy on it before:
beforeEach(function () {
...
$scope = $rootScope.$new();
spyOn($scope, '$emit');
ctrl = _$controller_('ExploreHeroController', { $scope: $scope });
...
});
describe('directive controller', function () {
it('should dispatch call $emit with $methodsBound', function () {
expect($scope.$emit).toHaveBeenCalledWith('$methodsBound');
});
});
I am trying to integrate Karma and Jasmine in to my project.
I have started off with a very basic test to ensure my controller is defined and a $scope variable equals a string - which pass as expected.
My controller, also calls a service which performed a $http.get, when running my test, without any mention of a service, i get the error:
Error: Unexpected request: GET /my/endpoint/
No more request expected
Controller:
define(['module'], function (module) {
'use strict';
var MyController = function ($scope, MyService) {
$scope.testScope = 'karma is working!';
MyService.getData().then(function (data) {
$scope.result = data.hour
});
};
module.exports = ['$scope', 'MyService', MyController ];
});
Test:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Are the above errors expected?
UPDATE from response below:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope, $httpBackend, myService;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _myService_) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.expectGET("/my/endpoint");
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Using Angular Mocks you will always get an error if there is an unexpected or incorrect http request attempted -- even for templates. In your case there are two ways to handle this for testing:
use $httpBackend
$httpBackend was designed for testing http requests without actually hitting the wire. In your test, simply add
$httpBackend.expectGET("/my/endpoint");
before you initialize the controller.
Mock the service
The service itself is making the http request, so you can mock the service instead. Services will be injected automatically as usual, but you can explicitly injection whatever you want:
controller = _$controller_('MyController', {$scope: scope,
MyService: {getData: () => ({then: () => {}}) });
This injects an object that has a getData function which returns an object with .then function. Of course this doesn't come close to implementing what you are trying to do, but it is another way to perform the test.
Both of the above approaches are valid. It depends on what you are testing and what you are trying to accomplish with the testing.
I am testing my angularjs application with Jasmine and Karma.
My test looks like this:
describe('Login test', function() {
// Mock our module in our tests
beforeEach(module('Project'));
var ctrl, scope;
// inject the $controller and $rootScope services
// in the beforeEach block
beforeEach(inject(function($controller, $rootScope) {
// Create a new scope that's a child of the $rootScope
scope = $rootScope.$new();
// Create the controller
ctrl = $controller('loginController', {
$scope: scope
});
}));
it('should get login success',function() {
scope.loginClick('user', 'pass');
});
});
I have a login controller with the loginClick function, and inside this function i have another function which is making a POST request. The problem is that the inner function is never executed, i try to console.log() to see if the function is called but with no success.
My function looks like this:
app.controller('loginController', ['$scope', '$http', '$route', function ($scope, $http, $route) {
console.log('controller call'); // yes it works
...
$scope.loginClick = function (username, password) {
console.log('controller call'); // yes it works
handler.reqPOST('/login', userData, function (result) {
console.log('login request'); // no output is sent to the console
});
};
}]);
The handler object is include in the karma configuration file at start-up.
First of all, unless you have very good reason, $http is the way to go in angularJS to call the back-end, it also makes it more testable.
In any case you should mock the post call, in a unit-test you don't want to rely on the back-end
In your case, you could use a spy (http://jasmine.github.io/2.0/introduction.html#section-Spies):
describe('Login test', function(){
beforeEach(module('Project'));
var ctrl, scope;
beforeEach(inject(function($injector) {
var $controller = $injector.get('$controller');
var $rootScope = $injector.get('$rootScope');
scope = $rootScope.$new();
ctrl = $controller('loginController', {
$scope: scope,
});
}));
it('test login', function () {
spyOn(handler, 'reqPOST');
scope.loginClick('user', 'pass');
expect(handler.reqPOST).toHaveBeenCalledWith('user', 'pass');
});
});
I have a service that synchronously returns data to a controller:
angular.module('app').controller(function($scope, myService) {
$scope.foo = myService.getFoo();
});
This works just fine in the browser. In my unit tests, $scope.foo is undefined:
beforeEach(function () {
module('app');
myService = jasmine.createSpyObj('myService', ['getFoo']);
inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ctrl = $controller('ModelSliderCtrl', {
myService: myService,
$scope: $scope
});
});
});
it('should have foo on the scope', function() {
myService.getFoo.and.returnValue({});
expect(myService.getFoo).toHaveBeenCalled(); // PASS
$scope.$digest();
expect($scope.foo).toBeDefined(); // FAIL - $scope.foo is undefined
});
This does work in both the browser and tests:
angular.module('app').controller(function($scope, myService) {
$scope.init = function() {
$scope.foo = myService.getFoo();
};
$scope.init();
});
.
it('should have foo on the scope', function() {
myService.getFoo.and.returnValue({});
$scope.init();
expect(myService.getFoo).toHaveBeenCalled(); // PASS
expect($scope.foo).toBeDefined(); // PASS
});
I'd like to believe I'm fairly well-versed in Angular, Jasmine and JavaScript. I've also asked some colleagues who are equally puzzled.
Can anyone explain to me what is going on here?
You are setting up a mock
it('should have foo on the scope', function() {
myService.getFoo.and.returnValue({});
after your controller has been instantiated. It's too late to set up the mock by then, do it before instantiating your controller since you are executing init() right away.
myService = jasmine.createSpyObj('myService', ['getFoo']);
myService.getFoo.and.returnValue({});
inject(function($controller, $rootScope) {
I have this simple controller, UserService is a service which return JSON
"use strict";
angular.module("controllers").controller('profileCtrl', ["$scope", "UserService",
function ($scope, UserService) {
$scope.current_user = UserService.details(0);
}
]);
I can not make the test. However this is my try
'use strict';
describe('profileCtrl', function () {
var scope, ctrl;
beforeEach(angular.mock.module('controllers'), function($provide){
$provide.value("UserService", {
details: function(num) { return "sdfsdf"; }
});
});
it('should have a LoginCtrl controller', function() {
expect(controllers.profileCtrl).toBeDefined();
});
beforeEach(angular.mock.inject(function($rootScope, $controller){
scope = $rootScope.$new();
$controller('profileCtrl', {$scope: scope});
}));
it('should fetch list of users', function(){
expect(controllers.scope.current_user.length).toBe(6);
expect(controllers.scope.current_user).toBe('sdfsdf');
});
});
The usage of $controller is correct, that's the way to instantiate a controller for a unit test. You can mock the UserService instance it gets directly in the $controller invocation.
You should be using its return value - this is the instance of your controller you're going to test.
You're trying to read stuff from controllers but its not defined anywhere in the test, I guess you're referring to the module.
This is how I would go about it + fiddle
//--- CODE --------------------------
angular.module('controllers', []).controller('profileCtrl', ["$scope", "UserService",
function ($scope, UserService) {
$scope.current_user = UserService.details(0);
}]);
// --- SPECS -------------------------
describe('profileCtrl', function () {
var scope, ctrl, userServiceMock;
beforeEach(function () {
userServiceMock = jasmine.createSpyObj('UserService', ['details']);
userServiceMock.details.andReturn('sdfsdf');
angular.mock.module('controllers');
angular.mock.inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('profileCtrl', {
$scope: scope,
UserService: userServiceMock
});
});
});
it('should have a LoginCtrl controller', function () {
expect(ctrl).toBeDefined();
});
it('should fetch list of users', function () {
expect(scope.current_user).toBe('sdfsdf');
});
});
You're welcome to change the fiddle online to see how it affects testing results.