Jasmine spyOn scope function breaks test - javascript

Case
When I create a spy on rootScope, the expectation fails for some reason. Check out the plunkr and try just commenting it out and reversing to see.
Code
Plunker Example
describe('Testing', function() {
var rootScope = null
//you need to indicate your module in a test
// beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
rootScope = $rootScope;
rootScope.value = false;
rootScope.testFn = function() {
rootScope.value = true;
}
}));
it('should modify root scope', function() {
// Creating this spy makes test fail
// Comment it outto make it pass
spyOn(rootScope, 'testFn');
expect(rootScope.value).toEqual(false);
rootScope.testFn();
expect(rootScope.value).toEqual(true);
});
});

You need to tell the spy to do something:
spyOn(rootScope, 'testFn').andCallThrough();
I updated the plnkr here: http://plnkr.co/edit/t3ksMtKSI3CEkCtpZ8tI?p=preview
Hope this helped!

Related

How to unit test an Angular 1 controller that contains DOM/jquery selector?

When using jasmine, I can't seem to test a function that has a jquery selector or document.getElementById in it. Is there a better strategy here? I normally do not put selectors in angular code, however this is a workaround.
In the function I'm testing:
this.scope.login = () => {
($('#login-form')[0] as HTMLFormElement).submit();
// or even
document.getElementById('login-form').submit(); // this is a workaround for browser auto-complete, I normally would not have selectors in angular code.
}
I get
TypeError: undefined is not an object (evaluating '$('#login-form')[0].submit')
I've tried "mocking by spying," using spyOn to try to mock the jquery selector function and return a fake element... but doesn't seem to work, or I'm not doing it right.
My spec loads the template (logs ok). Element is also defined and seems to be a valid angular-compiled element.
describe('Nav Controller Spec', function() {
beforeEach(function() {
angular.mock.module('App');
inject(function(_$controller_, $rootScope, _userModel_, _$q_, _$httpBackend_, $templateCache, _$compile_) {
scope = $rootScope.$new();
$q = _$q_;
$compile = _$compile_;
$httpBackend = _$httpBackend_;
deferred = _$q_.defer();
html = $templateCache.get('main/components/login/login.tpl.html');
var el = angular.element( html );
element = $compile( el )(scope); //element is valid and works
controller = _$controller_;
userModel = _userModel_;
scope.userModel = userModel;
// tried this... still get error
spyOn($.fn, 'val').and.returnValue('<form></form>');
//if i change to 'init' or 'find', i get 'undefined is not a constructor'
spyOn($.fn, 'init').and.returnValue('<form></form>');
ctrl = controller('loginController', { $scope: scope, $element: element });
$rootScope.$digest();
});
});
it('should login and change the status', function(){
spyOn( ctrl.scope.userModel, 'login' ).and.callThrough();
ctrl.scope.formType = 'login';
ctrl.scope.login(); //fails
expect( ctrl.scope.userModel.login ).toHaveBeenCalled();
});
As a last resort, i tried the following with document.getElementById('login-form') in the controller. However, I get TypeError: undefined is not a constructor (evaluating 'document.getElementById('login-form').submit()')
var mockElement = {
id:"login-form",
parentNode:true
};
var document_getElementById = document.getElementById;
var spy = spyOn(document, "getElementById").and.callFake(function(id){
if(id===mockElement.id){
return mockElement;
}
return document_getElementById(id);
});
Actually, this works. You will want to stub/spy with document.getElementById since that's what jquery uses under the hood. I only had forgotten to stub the submit function. I didn't realize this because jasmine's wrapped errors are so meaningless.
var mockElement = {
id:"login-form",
parentNode:true,
submit:function(){
return 'cheese';
}
};
var document_getElementById = document.getElementById;
var spy = spyOn(document, "getElementById").and.callFake(function(id){
if(id===mockElement.id){
return mockElement;
}
return document_getElementById(id);
});

Unit Testing a Controller with specific syntax

Whenever, I am testing a controller and have something like this in it.
$scope.isSomething = function (Item) {
return ItemCollection.someItem(Item.attachedItem);
};
giving error on karma console:
TypeError: undefined is not an object (evaluating 'Item.attachedItem')
I am simply calling the function from the test file like this:
scope.isSomething();
I need to mock the Item.attachedItem or I am missing something here.. Please Explain in details as this is happening in multiple files.. thanks in advance
Also, for this type of code
.controller('itemCtrl', function (itemCollection) {
var vm = this;
this.itemCollection= itemCollection;
itemCollection.someItem().then(function (Item) {
vm.pageUrl = Item.pageUrl;
vm.Item= Item.someItems;
});
});
Also, this is also part of the code for more broad view here it gives Item.pageUrl is not a object error
Refer angular unit testing docs
The ItemCollection being a service, you could inject a mock while initialising a controller using
var ItemCollection, ItemCrtl;
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCrtl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
For Item, the method isSomething should take care of checking if Item is undefined before doing Item.attachedItem
Testing an aync block is tricky. someItem returns a promise. $q an angular service to which can be used create async functions while testing.
We need to resolve the deferred object to test the async task.
var ItemCollection, ItemCrtl, deferedObj;
beforeEach(inject(function($controller, $rootScope, $q) {
$scope = $rootScope.$new();
deferedObj = $q.defer();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCollection.someItem.andReturn(deferedObj.promise);
ItemCtrl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
it('sets page url', function() {
deferedObj.resolve({ pageUrl: 'http://url', someItems: [1,2,3] });
scope.$apply();
expect(ItemCtrl.pageUrl).toEqual('http://url');
});
you have to use mock Item data in test like this (assuming attachedItem value is boolean)
var item={attachedItem:true}
scope.isSomething(item)
$scope.isSomething = function (Item) {
if(!Item.attachedItem){
Item.attachedItem=YOUR_MOCK_VALUE;
}
return ItemCollection.someItem(Item.attachedItem);
};

Reference error can't find variable $compile

I am unit testing an AngularJS directive with Jasmine.
I am getting this error even though I injected $compile in a beforeEach statement:
Reference Error: can't find variable: $compile
describe('test', function() {
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe('testCase', function() {
var nlElement = angular.element('<div directive></div>');
var element = $compile(nlElement)($rootScope); // this is where the error is being thrown
$rootScope.$digest();
it(...)
});
});
Do I have to include the statements in the second describe in the it blocks? Ultimately I want to be able to inject all three of those statements before each test, but I am trying to resolve the $compile error at the moment.
It turns out that the describe blocks are executed before the beforeEach statements, which is counter-intuitive to me. Also, if you want to initialize variables and your directive before your tests (like I did in the second describe block, then include it in a beforeEach statement, and test your assertions in it blocks.
describe('test', function() {
describe('testCase', function() {
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(inject(function() {
var nlElement = angular.element('<div directive></div>');
var element = $compile(nlElement)($rootScope); // this is where the error is being thrown
$rootScope.$digest();
}));
it(...)
});
});

How to unit test custom decorator with Jasmine (Angular js)

So I have such decorator in app config:
angular.module('app').config(['$provide', function ($provide) {
$provide.decorator('$rootScope', ['$delegate', function ($delegate) {
$delegate.constructor.prototype.$onRootScope = function (name, listener) {
var unsubscribe = $delegate.$on(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchRootScope = function (name, listener) {
var unsubscribe = $delegate.$watch(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchAfterLoad = function (watchExpression, listener, objectEquality) {
var initialLoad = true;
this.$watch(watchExpression, function () {
if (initialLoad) {
// note: this obviously runs outside of angular, so sometimes the timeout could run after initial load
setTimeout(function () { initialLoad = false; }, 25);
} else {
listener.apply(this, arguments);
}
}, objectEquality);
};
return $delegate;
}]);
}]);
As you can see this decorator lets me use $scope.$onRootScope instead of $rootScope.$on and takes care of automatic listeners removal on scope destroy event...
When I unit test my code which logic contains $scope.$onRootScope I'm getting such error: TypeError: undefined is not a constructor (evaluating 'scope.$onRootScope') in
Before each test I'm loading all required models and do inject which looks like this ~:
beforeEach(function () {
inject(function (_$rootScope_) {
$rootScope = _$rootScope_;
});
});
How should I overcome this problem?
Is there a way to mock / mimic $scope.$onRootScope behaviour?
I'm quite new to unit testing & Jasmine so sorry for not very nicely formatted question.
EDIT #1:
As I'm mocking my $scope object (var $scope = {...}) before passing it as argument to service method which I'm testing I can avoid error by simply defining $scope method:
$scope = {
...
$onRootScope: function() {}
}
Still awaiting for some better ideas :-)
I believe you need to build your $scope based off of the decorated $rootScope, as opposed to creating a new dummy object.
Like so:
var $root, $scope;
beforeEach(function () {
module('app');
inject(function ($rootScope) {
$root = $rootScope;
$scope = $root.$new();
});
});
it('should have the expected property', function () {
expect($scope.constructor.prototype).to.have.property('$watchRootScope');
});
I'll chuck in a link to the spec suite of a mini-lib I put together some time ago, doing roughly the same thing you are now.

Test Angular scope variables inside ajax request with Jasmine

I would like to know how to test some Angular scope variables at my controller that was created inside an ajax request.
What I mean is... This is my controller:
app.controller('NewQuestionCtrl', function ($scope, Question) {
loadJsonAndSetScopeVariables($scope, Question);
});
function loadJsonAndSetScopeVariables(scope, Question) {
Question.loadJson().then(function(success) {
var result = success.data.variables;
scope.levels = result.levels;
scope.tags = result.tags;
scope.difficulties = result.difficulties;
scope.questionTypes = result.questionTypes;
scope.areas = result.areas;
},function(data){
});
}
One of the prerequisites is not to use mock.
At my test I was able to inject my Question service:
describe('Controller: NewQuestionCtrl', function () {
beforeEach(angular.mock.module('testmeApp'));
var NewQuestionCtrl, scope, QuestionService;
beforeEach(inject(function ($controller, $rootScope, Question) {
scope = $rootScope.$new();
QuestionService = Question;
NewQuestionCtrl = $controller('NewQuestionCtrl', {
$scope: scope
});
}));
it('should attach a list of areas to the scope', function (done) {
expect(scope.areas).toBeDefined();
done();
});
Please, someone could help me?
Create a mock for Question and use that. There are several ways to do this. This is just one of them.
You could alternatively inject a real instance of Question and spy on that instead, but a mock is preferred to isolate these unit tests from the Question unit tests.
var questionDeferred, myController, scope;
var mockQuestion = {
loadJson: angular.noop
};
beforeEach(inject(function($q, $rootScope, $controller) {
questionDeferred = $q.defer();
scope = $rootScope.$new();
spyOn(mockQuestion, 'loadJson').and.returnValue(questionDeferred);
// Because your function is run straight away, you'll need to create
// your controller in this way in order to spy on Question.loadJson()
myController = $controller('NewQuestionCtrl', {
$scope: scope,
Question: mockQuestion
});
}));
it('should attach a list of areas to the scope', function (done) {
questionDeferred.resolve({/*some data*/});
scope.$digest();
expect(scope.areas).toBeDefined();
done();
});

Categories

Resources