Using Jasmnie's spyOn on $scope.$broadcast - angularjs

Im using jasmine's spyOn function to try to determine if $scope.$broadcast have been called or not.
girlnames.spec.js -the controller
describe('Girl names controller', function() {
var vm,
$scope;
beforeEach(module('nameStats'));
beforeEach(inject(function($controller, $rootScope, $q, _$httpBackend_, _namesService_) {
vm = $controller('girlNames', {
$scope: $rootScope.$new()
});
$scope = $rootScope.$new()
}));
it('addPersonManually should trigger $scope.$broadcast', function() {
spyOn($scope, '$broadcast').and.callThrough()
vm.addPersonManually(p)
$scope.$digest();
expect($scope.$broadcast).toHaveBeenCalled()
});
});
girlnames.js - the controller
"use strict";
angular.module('nameStats').controller('girlNames', girlNames);
girlNames.$inject = ['$scope', 'namesService'];
function girlNames($scope, namesService) {
var vm = this;
vm.addPersonManually = addPersonManually;
function addPersonManually(person) {
$scope.$broadcast('personSelected', person);
}
}
The output in the console:
Expected spy $broadcast to have been called.

Take a closer look at the way you are instantiating your controller
beforeEach(inject(function($controller, $rootScope, $q, _$httpBackend_, _namesService_) {
vm = $controller('girlNames', {
$scope: $rootScope.$new()
});
$scope = $rootScope.$new();
}));
You inject one scope instance and use a totally different one for testing.
Your code should look like this
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
vm = $controller('girlNames', {
$scope: $scope
});
}));
Advanced tip
Consider getting rid of local variables in your tests. Karma keeps references to all the test suits until all of them finish running thus causing a huge memory consumption. It can even cause process to fail if you have enough tests (it was a couple thousands in our case). Useful article.
Use this instead
beforeEach(inject(function($controller, $rootScope) {
this.$scope = $rootScope.$new();
this.ctrl = $controller('girlNames', {
$scope: $scope
});
}));
it('addPersonManually should trigger $scope.$broadcast', function() {
spyOn(this.$scope, '$broadcast').and.callThrough()
this.ctrl.addPersonManually(p)
this.$scope.$digest();
expect(this.$scope.$broadcast).toHaveBeenCalled()
});

Related

Can't find variable: $rootScope

I am new to unit testing and I am getting these errors even though I though my test was correct, I just cannot figure out what these errors mean and I have tried several things
Can't find variable: $rootScope
Error: Injector already created, can not register a module!
spec.js
describe('test broadcast', function () {
var $controller;
beforeEach(function() {
module('test');
inject(function (_$rootScope_, _$controller_) {
$rootScope = _$rootScope_;
spyOn($rootScope, '$broadcast');
// Notice how inject $controller here.
$controller = _$controller_;
});
});
it("should broadcast something", function ($rootScope) {
$controller('myCtrl', {
// Pass in the $rootScope dependency.
$rootScope: $rootScope.$new()
})
// Here we actually run the controller.
expect($rootScope.$broadcast).toHaveBeenCalledWith('update');
//someObj = { data: testData};
//expect($rootScope.$broadcast).toHaveBeenCalledWith('update', someObj);
});
})
controller
(function () {
var test= angular.module('test');
test.controller('myCtrl',
function($rootScope, $scope, $resource, $location, $route, $routeParams, $log, catalogData) {
$log.debug("myCtrl");
$log.debug(myCtrl);
$rootScope.$broadcast("update", {
data: testData
});
}); // catalogCtrl
})();
You have a variable called rootScope defined, not $rootScope - change your definition:
rootScope.$apply();
Though I personally like to define them like so:
var $rootScope;
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
EDIT 2:
You cannot access $rootScope in your it function because it is not in the current javascript scope (not angular $scope, don't get confused).
You need to define it alongside your controller at the top.
var $controller, $rootScope
And remove $rootScope from your it function so you don't overwrite it.
// Notice there is no $rootScope parameter.
it("should broadcast something", function () {
//Code
}
You will also have to pass in your other dependencies.
After a discussion with the OP, the whole code should look like this:
describe('test broadcast', function () { 
var $controller, $rootScope; 
beforeEach(function() { 
module('test'); 
inject(function (_$rootScope_, _ $controller_) { 
$rootScope = _$rootScope_; 
spyOn($rootScope, '$broadcast'); 
$controller = _$controller_; 
}); 
}); 
it("should broadcast something", function () { 
$controller('myCtrl', { 
$scope: $rootScope.$new(), 
catalogData: {} 
}) 
expect($rootScope.$broadcast).toHaveBeenCalledWith('update', {catalog:{}})}); 
})
EDIT 1:
You are passing in the $scope dependency. $broadcast is called on the $rootScope so you need to pass that in. Like this:
var testScope = $rootScope.$new()
$controller('myCtrl', {
// Pass in the $rootScope dependency.
$rootScope: testScope
}
Original post (in case it's still useful to anyone)
You aren't actually calling your controller anywhere in your test suite.
You need to have something like
var $controller
beforeEach(inject(function (_$rootScope_, _$controller_) {
$rootScope = _$rootScope_;
spyOn($rootScope, '$broadcast');
// Notice how inject $controller here.
$controller = _$controller_;
}));
Then initialise it in your test:
it("should broadcast something", function () {
// Here we actually run the controller.
$controller('myCtrl', {
// Pass in the $rootScope dependency.
$rootScope: $rootScope.$new()
}
expect($rootScope.$broadcast).toHaveBeenCalledWith('catalogUpdate');
someObj = { catalog: catalogData};
expect($rootScope.$broadcast).toHaveBeenCalledWith('catalogUpdate', someObj);
});
This will remove the error about $rootScope.broadcast not being called.
Take a look at the "Testing Controllers" section here: https://docs.angularjs.org/guide/controller
As for not being able to register a module, this normally happens if you have an inject() before a beforeEach(module('abc')).
As the error says, you cannot register another module after inject has been called.

Mocking a service dependency in an Angular controller with jasmine

I've been trying to get started with unit testing in angular with karma and jasmine, and i've been pulling my hair out trying to wrap my head around how to test controllers with dependencies. I tried mocking a spy with a jasmine spyObj and registering it in the beforeEach hook, but for some reason the spy isn't being recognized.
Here's the code:
angular.module('testModule', [])
.controller('TestController', [
'$scope',
'TestService',
function ($scope, TestService) {
$scope.data = TestService.load();
}])
.factory('TestService', function () {
return {
load: function(){
return "foo";
}
}
});
and here's the test
describe('TestController', function() {
var $controller, $scope, TestService;
beforeEach(module('testModule'), function($provide){
TestService = jasmine.createSpyObj("TestService", ["load"]);
TestService.load.andReturn("bar");
$provide.value("TestService", TestService)
});
beforeEach(inject(function(_$controller_, $rootScope, _TestService_) {
$scope = $rootScope.$new();
TestService = _TestService_;
$controller = _$controller_('TestController', {
$scope: $scope,
TestService: TestService
});
}));
it('should set $scope.data to bar when TestService.load is called', function() {
expect(TestService.load).toHaveBeenCalled();
expect($scope.data).toEqual("bar");
}); });
Both assertions in the test fail.
I get 'Error: Expected a spy, but got Function' when i call expect(TestService.load).toHaveBeenCalled();
and if I call expect($scope.data).toEqual("bar"), I get Expected 'foo' to equal 'bar'. "Foo" is coming from the actual service, not the spy object.
Thanks for your help.
Instead of jasmine.createSpyObj, it will be easier to use the existing service that the $injector provides and then just mock the single method. You can achieve this with spyOn instead:
describe('TestController', function() {
var $controller, $scope, TestService;
beforeEach(module('testModule'));
beforeEach(inject(function(_$controller_, $rootScope, _TestService_) {
$scope = $rootScope.$new();
TestService = _TestService_;
spyOn(TestService, 'load').and.returnValue('bar');
$controller = _$controller_('TestController', {
$scope: $scope,
TestService: TestService
});
}));
it('should set $scope.data to bar when TestService.load is called', function() {
expect(TestService.load).toHaveBeenCalled();
expect($scope.data).toEqual("bar");
});
});
In your beforeEach you are injecting in _TestService_ and then overwriting the one you declared in the previous beforeEach via:
TestService = _TestService_;
Remove that code and your test should succeed.
Also there is no need to do this:
$provide.value("TestService", TestService)
Basically you're trying to use Angular's dependency injection when you're manually injecting things which is unnecessary.

Test Angular controller calls service function exactly once using Jasmine

I have the following...
app.controller('testCtrl', function(testService){
testService.doSomething();
});
app.service('testService', function(){
this.doSomething = function(){...};
});
I want to use Jasmine to ensure doSomething is called once and only once. I seem to be having some trouble doing this.
Also, I am currently grabbing my controller from a compiled element like this...
var element = angular.element('<my-test-directive />');
controller = view.controller('testCtrl');
So extra appreciation if it fits with this sort of formatting
Update
I tried this...
describe("Testing", function () {
var $rootScope,
$scope,
$compile,
testService,
view,
$controller;
beforeEach(module("app"));
function createController() {
return $controller('testCtrl', {
$scope: scope,
testService:testService
});
}
function SetUpScope(_$controller_, _$compile_, _$rootScope_, _testService_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_;
testService = _testService_;
spyOn(testService, 'doSomething');
}
SetUpScope.$inject = ["$controller","$compile", "$rootScope", "testService"];
beforeEach(inject(SetUpScope));
it("On intitialization, the controller should register itself with the list service", function(done){
createController();
scope.$digest();
expect(workOrderService.doSomething).toHaveBeenCalled();
})
});
It seems to work
It is probably better to test controller in isolation and use Jasmine spies for this:
spyOn(testService, 'doSomething');
expect(testService.doSomething.calls.count()).toEqual(0);
Something like this should work in the actual test.
describe('testCtrl function', function() {
describe('testCtrl', function() {
var $scope, testService;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller, _testService_) {
$scope = $rootScope.$new();
testService = _testService_;
spyOn(testService, 'doSomething');
$controller('MyController', {$scope: $scope});
}));
it('should call testService.doSomething()', function() {
expect(testService.doSomething.calls.count()).toEqual(1);
});
});
});
Here is a quick plunkr http://plnkr.co/edit/Swso4Y
Depending on which version of Jasmine you are using you might need to use
expect(testService.doSomething.calls.length).toEqual(1);

Mocking $routeParams in a test in order to change its attributes dynamically

I have a controller test that depends on the Angular $routeParams service:
var $routeParams, MainCtrl, scope;
beforeEach(inject(function ($controller, $rootScope, $injector, $templateCache) {
scope = $rootScope.$new();
$routeParams = $injector.get('$routeParamsMock');
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: $routeParams,
});
}));
it('should load a pg from $routeParams', function(){
scope.userData = {};
$routeParams._setPg('PG_FIRST');
scope.$digest();
timeout.flush();
expect(scope.userData.pg).toBe(0);
$routeParams._setPg('PG_SECOND');
scope.$digest();
timeout.flush();
expect(scope.userData.pg).toBe(1);
});
$routeParamsMock:
!(function(window, angular){
'use strict';
angular.module('vitaApp')
.service('$routeParamsMock', function() {
var _pg = null;
return{
pg: _pg,
_setPg: function(pg){
_pg = pg;
}
}
});
})(window, window.angular);
When debugging the test, I was surprised to find out that $routeParamsMock.pg was returning null every single time, even though I called _setPg with a different value.
Is it because null is considered a primitive (with a type of object...), and thus passed by value?, or perhaps because Angular is copying the object that is passed to the $controller service?.
The solution I am looking for is preferably one that won't require to instanciate different controllers per different test scenerios.
eg:
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: {'pg': 'PG_FIRST'},
});
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: {'pg': 'PG_SECOND'},
});
The thing is, what you don't want to do, is probably the best solution you have. A mock makes sense when what you want to mock is kinda complex. Complex dependency with methods, lot of states, etc. For a simple object like $routeParams it makes all the sense of the world to just pass a dummy object to it. Yes it would require to instantiate different controllers per test, but so what?
Structure your tests in a way that makes sense, makes it readable and easy to follow.
I suggest you something like:
describe('Controller: Foo', function() {
var $controller, $scope;
beforeEach(function() {
module('app');
inject(function($rootScope, _$controller_) {
$scope = $rootScope.$new();routeParams = {};
$controller = _$controller_;
});
});
describe('With PG_FIRST', function() {
beforeEach(function() {
$controller('Foo', { $scope: $scope, $routeParams: {'PG': 'PG_FIRST'}});
});
it('Should ....', function() {
expect($scope.something).toBe('PG_FIRST');
});
});
describe('With PG_SECOND', function() {
beforeEach(function() {
$controller('Foo', { $scope: $scope, $routeParams: {'PG': 'PG_SECOND'}});
});
it('Should ....', function() {
expect($scope.something).toBe('PG_SECOND');
});
});
});
With a good test organization, I can say that I like this test easy to follow.
http://plnkr.co/edit/5Q3ykv9ZB7PuGFMfWVY5?p=preview

AngularJS + Jasmine Noob: How to access $scope in spec?

This is my first time testing using Jasmine. I'm having trouble accessing the $scope variables in the spec. I have a failing test:
mysite ProductsDetailCtrl sets hey
Expected undefined to be 1.
Error: Expected undefined to be 1.
spec:
//= require helpers/load-angular-mysite-module
//= require products/controllers/products_detail_controller
describe('mysite', function() {
var $rootScope, $scope, $controller;
beforeEach(function() {
module('mysite');
});
describe('ProductsDetailCtrl', function() {
beforeEach(inject(function(_$rootScope_, _$controller_) {
$rootScope = _$rootScope_; // don't really
$scope = $rootScope.$new(); // understand what's
$controller = _$controller_; // going on in this function
controller = $controller('ProductsDetailCtrl', {
'$rootScope': $rootScope,
'$scope': $scope
});
}));
it('sets hey', function() {
expect($rootScope.hey).toBe(1);
});
});
});
controller:
app.controller('ProductsDetailCtrl', ['$scope', '$resource', function($scope, $resource) {
$scope.hey = 1;
....
Could someone explain to me how I would access the scope?
You just have to check for the property heyin your $scope not in the $rootScope:
describe('mysite', function() {
var scope, ProductsDetailCtrl;
beforeEach(function() {
module('mysite');
});
describe('ProductsDetailCtrl', function() {
beforeEach(inject(function($controller, $rootScope) {
// Create a mock scope for your controller.
scope = $rootScope.$new();
// Initialize the controller with the mocked scope.
ProductsDetailCtrl = $controller('ProductsDetailCtrl', {
$scope: scope
});
}));
it('sets hey', function() {
expect(scope.hey).toBe(1);
});
});
});

Resources