I have a basic controller that uses $stateParams.
angular.module('example')
.controller('SampleCtrl', ['$stateParams',
function($stateParams) {
var vm = this;
vm.isSomething = ($stateParams.isSomething === 'true') ? true : false;
}
]);
In my unit test, I need to set the $stateParams.isSomething to true in one test, and false in another test.
describe('SampleCtrl Test', function() {
var ctrl, scope, $stateParams;
// set default stateParams
$stateParams = { isSomething: 'false' };
beforeEach(function(){
inject(function($rootScope, $controller){
scope = $rootScope.$new();
ctrl = $controller('SampleCtrl', {
$scope: scope,
$stateParams: $stateParams
});
});
});
describe('when isSomething is false', function() {
it('should be false', function() {
expect(ctrl.isSomething).toBe(false);
});
});
describe('when isSomething is true', function() {
beforeEach(function() {
$stateParams.isSomething = 'true';
});
it('should be true', function() {
// THIS IS FAILING
expect(ctrl.isSomething).toBe(true);
});
});
});
How can I properly mock different states of $stateParams for different tests?
I think you would need to instantiate the controller again with the updated scope object.
You also have a few naming issues as well, see the comments in the code below.
DEMO
describe('SampleCtrl Test', function() {
var ctrl, scope, $stateParams, $controller;
// set default stateParams
// you have called it 'something' in your controller not 'isSomething'
$stateParams = { something: 'false' };
beforeEach(function(){
// load module
module('example');
inject(function($rootScope, _$controller_){
scope = $rootScope.$new();
// angular removes the _'s for you so you can call it $controller
$controller = _$controller_;
ctrl = $controller('SampleCtrl', {
$scope: scope,
$stateParams: $stateParams
});
});
});
describe('when isSomething is false', function() {
it('should be false', function() {
expect(ctrl.isSomething).toBe(false);
});
});
describe('when isSomething is true', function() {
beforeEach(function() {
// you have called it 'something' in your controller not 'isSomething'
$stateParams.something = 'true';
// instantiate a new controller with the updated $stateParams object
ctrl = $controller('SampleCtrl', {
$scope: scope,
$stateParams: $stateParams
});
});
it('should be true', function() {
// THIS IS FAILING
expect(ctrl.isSomething).toBe(true);
});
});
});
The issue you are having is that the beforeEach of the describe('when isSomething is true') evaluates after the beforeEach of the describe('SampleCtrl Test'), i.e. the controller is already instantiated when the $stateParams values are changed. As Matt wrote in his answer you need to instantiate the controller after the change has taken place.
I usually use a simple workaround to this. Since the beforeEach is instantiated after the controller, I usually write an empty it-statement wherein the incorrect stateParams were sent to the controller and ignore that one. For all preceeding it-statements the change to stateParams changes will have taken place and the controller will then use the correct variables:
describe('when isSomething is true', function() {
beforeEach(function() {
$stateParams.isSomething = 'true';
});
it('should take one iteration to set the state parameter', function() {
// do nothing as the controller will not use the new $stateParams yet
});
it('should be true', function() {
// THIS IS NO LONGER FAILING
expect(ctrl.isSomething).toBe(true);
});
});
While not the most beautiful solution it seems to be the simplest
Related
function in controller:
angular.module('myApp').controller('MyController', function(){
$scope.f = function($event){
$event.preventDefault();
//logic
return data;
}
})
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
})
it('should...', function(){
//fire event and expect data
})
$scope.f function is used in directive, it can be executed by ng-click="f($event)"
what is right way for fire event in unit test?
Short Answer
You don't need to fire the event. You have access to the scope, which has the function you want to test. This means you just execute the function, then assert. It will look something like this:
it('should call preventDefault on the given event', function(){
var testEvent = $.Event('someEvent');
$scope.f(testEvent);
expect(testEvent.isDefaultPrevented()).toBe(true);
});
See the following:
jQuery Event Object
event.isDefaultPrevented()
Full Spec
Also - your it block should be inside your describe block, so that it has access to the $scope field. It should look more like this:
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
it('should call preventDefault on the given event', function(){
var testEvent = $.Event('someEvent');
$scope.f(testEvent);
expect(testEvent.isDefaultPrevented()).toBe(true);
});
})
A Note About Structure
Don't be afraid to use the describe blocks to structure your tests. Imagine you had another function on the $scope called f2, then you would probably want to partition your spec file up more like this:
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
describe('$scope', function() {
describe('.f()', function() {
// tests related to only the .f() function
});
describe('.f2()', function() {
// tests related to only the .f2() function
});
});
})
This has the benefit that when a test fails, the error message you see is constructed based on the hierarchy of describe blocks. So it would be something like:
MyController $scope .f() should call preventDefault on the given
event
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');
});
});
So I have my blank tests passing with this setup.
describe('loginController', function() {
var scope, createController;
beforeEach(module('souply'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
$location = _$location_;
scope = $rootScope.$new();
createController = function() {
return $controller('loginController', {
'$scope': scope
});
};
}));
And here are the tests...
describe('processGoogleLogin', function(){
describe('successful', function(){
beforeEach(function() {
});
it('should connect to google plus', function () {
expect(true).toBe(true);
});
This one passes no problem.
QUESTION: How can I test a method on the login controller?
Here is the method I want to test on the login controller:
$scope.processGoogleLogin = function(){
console.log('process login was clicked');
window.location.replace('/#/dashboard');
};
The test I have so far is:
it('should sign you into the dashboard', function () {
scope.processGoogleLogin();
//$controller.processGoogleLogin();
//expect(window.location).toBe('/#/dashboard');
});
This test throws an error of:
'undefined' is not a function (evaluating 'scope.processGoogleLogin()')
This needed this line.
var ctrl = $controllerConstructor('myController', {$scope: scope, myResolve: {}, state: state});
I am unit testing a controller and I want to test an event handler. Say my controller looks like:
myModule.controller('MasterController', ['$scope', function($scope){
$scope.$on('$locationChangeSuccess', function() {
$scope.success = true;
});
}]);
Would I broadcast that in my Jasmine test? Would I emit it? Is there an accepted standard?
The solution I came up with is as follows:
describe('MasterController', function() {
var $scope, $rootScope, controller, CreateTarget;
beforeEach(function() {
inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
CreateTarget = function() {
$controller('MasterController', {$scope: $scope});
}
});
});
describe('$locationChangeSuccess', function() {
it('should set $scope.success to true', function() {
controller = CreateTarget();
$rootScope.$broadcast('$locationChangeSuccess');
expect($scope.success).toBe(true);
});
});
});
I don't think there is "an accepted standard" but according to $location source code the event is broadcasted, so I would mock this behavior and test it this way:
'use strict';
describe('MasterController', function() {
var MasterController,
$rootScope,
$scope;
beforeEach(module('myModule'));
beforeEach(inject(function($rootScope, $injector, $controller) {
$rootScope = $rootScope;
$scope = $rootScope.$new();
MasterController = $controller('MasterController', {
'$scope': $scope
});
$scope.$digest();
}));
describe('$locationChangeSuccess event listener', function() {
it('should set $scope.success to true', function() {
var newUrl = 'http://foourl.com';
var oldUrl = 'http://barurl.com'
$scope.$apply(function() {
$rootScope.$broadcast('$locationChangeSuccess', newUrl, oldUrl);
});
expect($scope.success).toBe(true);
});
});
});
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) {