I'm trying to test a function in my controller that first watches for a dictionary to be loaded before it takes any action.
The problem i am getting is that my test fails because the watch doesn't appear to run.
My Controller Function
function setPageTitle(title) {
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
if ($location.$$path != '/dashboard') {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles[title] || $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
} else {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
}
}
});
}
My Test...
describe('AppController function', function() {
var rootScope, scope, $location, $window, controller, createController, cacheFactory, toastr;
beforeEach(module('mockedDashboard'));
beforeEach(inject(function(_$rootScope_, $controller, _$location_, _$window_, _toastr_, _$timeout_) {
$location = _$location_;
$window = _$window_;
$timeout = _$timeout_;
scope = _$rootScope_.$new();
rootScope = _$rootScope_;
toastr = _toastr_;
createController = function() {
return $controller('AppController', {
'$scope': scope
});
};
controller = createController();
}));
// We are using CacheFactory in this project, when running multiple tests on the controller
// we need to destroy the cache for each test as the controller is initialized for each test.
afterEach(inject(function(_CacheFactory_) {
cacheFactory = _CacheFactory_;
cacheFactory.destroy('defaultCache');
}));
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
});
the failure message
Expected undefined to be 'LOG IN'
rootScope.pageTitle is never set because the watch doesn't run when the test calls the function. How can i get around this?
I tried scope.$apply() which i read should trigger the $watch, but it still doesn't work.
EDIT
I tried using the done function, however the test still fails because rootScope.pageTitle still appears to remain undefined. 3000ms should be ample time for this to work, usually this is done in less that 500ms. (i also know that the code works because this test is being written too late)
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function(done) {
scope.setPageTitle('logIn');
scope.$apply();
setTimeout(function() {
// console.log('dictionary: ' + rootScope.dictionary.pageTitles.logIn);
console.log('rootScope.pageTitle: ' + rootScope.pageTitle);
expect(rootScope.pageTitle).toBe('LOG IN');
// expect(true).toBe(false);
done();
}, 3000);
});
});
Well, it is pretty obvious - your test is failing because the code ... is not running.
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
...
}
});
Most probably the problem is in $rootScope.dictionary being undefined due to not being explicitly set for testing purposes.
Try this
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
rootScope.dictionary = {
'whatever you': 'need here'
};
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
And by the way - there is absolutely no need in using async test case, as $digest will be invoked syncronously at scope.$apply(); line.
It looks like your test is a synchronous test, and therefore Karma thinks it's done as soon as the function returns. Unfortunately, this isn't actually the case since you need to wait around for the affects of the $apply to occur, which takes another pass through the event loop. You'll want to turn your test into an asynchronous test by adding a done parameter and calling it somehow after your watch has been triggered.
it('description', function(done) {
// do your setup things here
scope.$apply();
setTimeout(function() {
// make your assertions here
done()
}, 25);
})
Of course, using setTimeout is a pretty ugly way to wait the necessary time, and if there's some other event you can watch for, it would be more elegant.
Few small changes done on your test case, you can give a try for this,
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
rootScope.dictionary = "someValue"; // As you were watching this object assign some value and then apply $digest
// Instead of $apply use $digest with rootscope
rootscope.$digest();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
Related
How do I mock the timeout call, here?
$scope.submitRequest = function () {
var formData = getData();
$scope.form = JSON.parse(formData);
$timeout(function () {
$('#submitForm').click();
}, 2000);
};
I want to see timeout has been called with the correct function.
I would like an example of the spyon function mocking $timeout.
spyOn(someObject,'$timeout')
First of all, DOM manipulation should only be performed in directives.
Also, it's better to use angular.element(...), than $(...).
Finally, to do this, you can expose your element's click handler to the scope, spy on it, and check if that handler has been called:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.myClickHandler).toHaveBeenCalled();
EDIT:
since that's a form and there is no ng-click handler, you can use ng-submit handler, or add a name to your form and do:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.formName.$submitted).toBeTruthy();
$timeout can be spied or mocked as shown in this answer:
beforeEach(module('app', ($provide) => {
$provide.decorator('$timeout', ($delegate) => {
var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
// methods aren't copied automatically to spy
return angular.extend(timeoutSpy, $delegate);
});
}));
There's not much to test here, since $timeout is called with anonymous function. For testability reasons it makes sense to expose it as scope/controller method:
$scope.submitFormHandler = function () {
$('#submitForm').click();
};
...
$timeout($scope.submitFormHandler, 2000);
Then spied $timeout can be tested:
$timeout.and.stub(); // in case we want to test submitFormHandler separately
scope.submitRequest();
expect($timeout).toHaveBeenCalledWith(scope.submitFormHandler, 2000);
And the logic inside $scope.submitFormHandler can be tested in different test.
Another problem here is that jQuery doesn't work well with unit tests and requires to be tested against real DOM (this is one of many reasons why jQuery should be avoided in AngularJS applications when possible). It's possible to spy/mock jQuery API like shown in this answer.
$(...) call can be spied with:
var init = jQuery.prototype.init.bind(jQuery.prototype);
spyOn(jQuery.prototype, 'init').and.callFake(init);
And can be mocked with:
var clickSpy = jasmine.createSpy('click');
spyOn(jQuery.prototype, 'init').and.returnValue({ click: clickSpy });
Notice that it's expected that mocked function will return jQuery object for chaining with click method.
When $(...) is mocked, the test doesn't require #submitForm fixture to be created in DOM, this is the preferred way for isolated unit test.
Create mock for $timeout provider:
var f = () => {}
var myTimeoutProviderMock = () => f;
Use it:
beforeEach(angular.mock.module('myModule', ($provide) => {
$provide.factory('$timeout', myTimeoutProviderMock);
}))
Now you can test:
spyOn(f);
expect(f).toHaveBeenCalled();
P.S. you'd better test result of function in timeout.
Assuming that piece of code is within the controller or being created in the test by $controller, then $timeout can be passed in the construction parameter. So you could just do something like:
var timeoutStub = sinon.stub();
var myController = $controller('controllerName', timeoutStub);
$scope.submitRequest();
expect(timeoutStub).to.have.been.called;
Unit Tesitng $timeout with flush delay
You have to flush the queue of the $timeout service by calling $timeout.flush()
describe('controller: myController', function(){
describe('showAlert', function(){
beforeEach(function(){
// Arrange
vm.alertVisible = false;
// Act
vm.showAlert('test alert message');
});
it('should show the alert', function(){
// Assert
assert.isTrue(vm.alertVisible);
});
it('should hide the alert after 5 seconds', function(){
// Act - flush $timeout queue to fire off deferred function
$timeout.flush();
// Assert
assert.isFalse(vm.alertVisible);
});
})
});
Please checkout this link http://jasonwatmore.com/post/2015/03/06/angularjs-unit-testing-code-that-uses-timeout
I totally agree with Frane Poljak's answer. You should surely follow his way. Second way to do it is by mocking $timeout service like below:
describe('MainController', function() {
var $scope, $timeout;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller, $injector) {
$scope = $rootScope.$new();
$timeout = jasmine.createSpy('$timeout');
$controller('MainController', {
$scope: $scope,
$timeout: $timeout
});
}));
it('should submit request', function() {
$scope.submitRequest();
expect($timeout).toHaveBeenCalled();
});
Here is the plunker having both approaches: http://plnkr.co/edit/s5ls11
I know when you use spyOn you can have different forms like .and.callFake or .andCallThrough. I'm not really sure which one I need for this code I'm trying to test...
var lastPage = $cookies.get("ptLastPage");
if (typeof lastPage !== "undefined") {
$location.path(lastPage);
} else {
$location.path('/home'); //TRYING TO TEST THIS ELSE STATEMENT
}
}
Here is some of my test code:
describe('Spies on cookie.get', function() {
beforeEach(inject(function() {
spyOn(cookies, 'get').and.callFake(function() {
return undefined;
});
}));
it("should work plz", function() {
cookies.get();
expect(location.path()).toBe('/home');
expect(cookies.get).toHaveBeenCalled();
expect(cookies.get).toHaveBeenCalledWith();
});
});
I've tried a lot of different things, but I'm trying to test the else statement. Therefore I need to make cookies.get == undefined.
Everytime I try to do that though, I get this error:
Expected '' to be '/home'.
The value of location.path() never changes when cookies.get() is equal to undefined. I think I'm using the spyOn incorrectly?
Follow-up on my mock values:
beforeEach(inject(
function(_$location_, _$route_, _$rootScope_, _$cookies_) {
location = _$location_;
route = _$route_;
rootScope = _$rootScope_;
cookies = _$cookies_;
}));
Follow-up on the functions:
angular.module('buildingServicesApp', [
//data
.config(function($routeProvider) {
//stuff
.run(function($rootScope, $location, $http, $cookies)
No names on these functions, therefore how do I call the cookies.get?
Right now, you're testing that the location.path() function works as designed. I'd say you should leave that testing to the AngularJS team :). Instead, verify that the function was called correctly:
describe('Spies on cookie.get', function() {
beforeEach((function() { // removed inject here, since you're not injecting anything
spyOn(cookies, 'get').and.returnValue(undefined); // As #Thomas noted in the comments
spyOn(location, 'path');
}));
it("should work plz", function() {
// cookies.get(); replace with call to the function/code which calls cookies.get()
expect(location.path).toHaveBeenCalledWith('/home');
});
});
Note that you shouldn't be testing that your tests mock cookies.get, you should be testing that whatever function calls the first bit of code in your question is doing the right thing.
Learning jasmine for the first time and I am stuck on this error when trying to test the focus() functionality in an angular service.
Here is the service:
myApp.service('MyService', function($timeout, $window) {
var service = {
focusElem: focusElem
};
return service;
function focusElem(id) {
console.log('id of element is = ', id);
if (id) {
$timeout(function() {
var element = $window.document.getElementById(id);
console.log('element is = ', element);
if (element) {
element.focus();
}
});
}
};
});
Here is my spec file
describe('myApp', function() {
var element, dummyElement;
beforeEach(function() {
// Initialize myApp injector
module('myApp');
// Inject instance of service under test
inject(function($injector) {
MyServiceObj = $injector.get('MyService');
});
element = angular.element('<input id="firstName" name="firstName"/>');
dummyElement = document.createElement('input');
dummyElement.setAttribute('id', 'lastName');
});
it('should have focus if the focus Service is used on an element', function() {
console.info('------------------');
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
expect(dummyElement.focus).toHaveBeenCalled();
});
});
My error:
myApp should have focus if the focus Service is used on an element
Expected spy focus to have been called.
Error: Expected spy focus to have been called.
If you are using ngMock many services are changed so they can be controlled in a synchronous manner within test code to give you more control over the flow.
One of the affected services is $timeout.
The function passed to $timeout inside your service will not execute in your test unless you tell it to.
To tell it to execute use $timeout.flush() like this:
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
Note that you need a reference to the $timeout service:
var element, dummyElement, $timeout;
beforeEach(function() {
module('myApp');
inject(function($injector, _$timeout_) {
MyServiceObj = $injector.get('MyService');
$timeout = _$timeout_;
});
The next problem is due to the following line in your service:
var element = $window.document.getElementById(id);
The elements you create in your test are never attached to the DOM, so the service will not find them.
The easiest solution is to just attach your elements to the DOM. In this case it's important that you remove them manually after the test, since Jasmine uses the same DOM for your entire test suite.
For example:
it('should have focus if the focus Service is used on an element', function() {
var body = angular.element(document.body);
body.append(element);
body.append(dummyElement);
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
element.remove();
dummyElement.remove();
});
Demo: http://plnkr.co/edit/F8xqfYYQGa15rwuPPbN2?p=preview
Now, attaching and removing elements to the DOM during unit tests are not always a good thing to do and can get messy.
There are other ways to handle it, for example by spying on getElementById and controlling the return value or by mocking an entire document. I won't go into that here however as I'm sure there are examples of it around here already.
I'm trying to write tests for a method that returns an angular promise ($q library).
I'm at a loss. I'm running tests using Karma, and I need to figure out how to confirm that the AccountSearchResult.validate() function returns a promise, confirm whether the promise was rejected or not, and inspect the object that is returned with the promise.
For example, the method being tested has the following (simplified):
.factory('AccountSearchResult', ['$q',
function($q) {
return {
validate: function(result) {
if (!result.accountFound) {
return $q.reject({
message: "That account or userID was not found"
});
}
else {
return $q.when(result);
}
}
};
}]);
I thought I could write a test like this:
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("message"); // PASSES
});
That passes, but so does this (erroneously):
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("I_DONT_EXIST"); // PASSES, should fail
});
I am trying to use the chai-as-promised 'eventually', but all my tests pass with false positives:
it("it should return an object", function () {
promise = AccountSearchResult.validate();
expect(promise).to.eventually.be.an('astronaut');
});
will pass. In looking at docs and SO questions, I have seen examples such as:
expect(promise).to.eventually.to.equal('something');
return promise.should.eventually.equal('something');
expect(promise).to.eventually.to.equal('something', "some message about expectation.");
expect(promise).to.eventually.to.equal('something').notify(done);
return assert.becomes(promise, "something", "message about assertion");
wrapping expectation in runs() block
wrapping expectation in setTimeout()
Using .should gives me Cannot read property 'eventually' of undefined. What am I missing?
#runTarm 's suggestions were both spot on, as it turns out. I believe that the root of the issue is that angular's $q library is tied up with angular's $digest cycle. So while calling $apply works, I believe that the reason it works is because $apply ends up calling $digest anyway. Typically I've thought of $apply() as a way to let angular know about something happening outside its world, and it didn't occur to me that in the context of testing, resolving a $q promise's .then()/.catch() might need to be pushed along before running the expectation, since $q is baked into angular directly. Alas.
I was able to get it working in 3 different ways, one with runs() blocks (and $digest/$apply), and 2 without runs() blocks (and $digest/$apply).
Providing an entire test is probably overkill, but in looking for the answer to this I found myself wishing people had posted how they injected / stubbed / setup services, and different expect syntaxes, so I'll post my entire test.
describe("AppAccountSearchService", function () {
var expect = chai.expect;
var $q,
authorization,
AccountSearchResult,
result,
promise,
authObj,
reasonObj,
$rootScope,
message;
beforeEach(module(
'authorization.services', // a dependency service I need to stub out
'app.account.search.services' // the service module I'm testing
));
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_; // native angular service
$rootScope = _$rootScope_; // native angular service
}));
beforeEach(inject(function ($injector) {
// found in authorization.services
authObj = $injector.get('authObj');
authorization = $injector.get('authorization');
// found in app.account.search.services
AccountSearchResult = $injector.get('AccountSearchResult');
}));
// authObj set up
beforeEach(inject(function($injector) {
authObj.empAccess = false; // mocking out a specific value on this object
}));
// set up spies/stubs
beforeEach(function () {
sinon.stub(authorization, "isEmployeeAccount").returns(true);
});
describe("AccountSearchResult", function () {
describe("validate", function () {
describe("when the service says the account was not found", function() {
beforeEach(function () {
result = {
accountFound: false,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// USING APPLY... this was the 'magic' I needed
$rootScope.$apply();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
it("should have entered the 'catch' function", function () {
expect(message).to.equal("PROMISE REJECTED");
});
it("should return an object with a message property", function () {
expect(reasonObj).to.have.property("message");
});
// other tests...
});
describe("when the account ID was falsey", function() {
// example of using runs() blocks.
//Note that the first runs() content could be done in a beforeEach(), like above
it("should not have entered the 'then' function", function () {
// executes everything in this block first.
// $rootScope.apply() pushes promise resolution to the .then/.catch functions
runs(function() {
result = {
accountFound: true,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
reasonObj = arg;
message = "PROMISE REJECTED";
});
$rootScope.$apply();
});
// now that reasonObj has been populated in prior runs() bock, we can test it in this runs() block.
runs(function() {
expect(reasonObj).to.not.equal("PROMISE RESOLVED");
});
});
// more tests.....
});
describe("when the account is an employee account", function() {
describe("and the user does not have EmployeeAccess", function() {
beforeEach(function () {
result = {
accountFound: true,
accountId: "160515151"
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// digest also works
$rootScope.$digest();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
// more tests ...
});
});
});
});
});
Now that I know the fix, it is obvious from reading the $q docs under the testing section, where it specifically says to call $rootScope.apply(). Since I was able to get it working with both $apply() and $digest(), I suspect that $digest is really what needs to be called, but in keeping with the docs, $apply() is probably 'best practice'.
Decent breakdown on $apply vs $digest.
Finally, the only mystery remaining to me is why the tests were passing by default. I know I was getting to the expectations (they were being run). So why would expect(promise).to.eventually.be.an('astronaut'); succeed? /shrug
Hope that helps. Thanks for the push in the right direction.
I want to test my AngularJS alert service like this (using Mocha and Chai):
describe('service', function() {
var alertService;
var $rootScope;
beforeEach(module('components.services'));
beforeEach(inject(function(_alertService_, _$rootScope_) {
alertService = _alertService_;
$rootScope = _$rootScope_;
}));
describe('alertService', function() {
it('should start with zero alerts', function() {
$rootScope.should.have.property('alerts').with.length(0);
});
it('should add an alert of type danger', function() {
alertService.add('danger', 'Test Alert!');
$rootScope.should.have.property('alerts').with.length(1);
});
it('should add an alert of type warning', function() {
alertService.add('warning', 'Test Alert!');
$rootScope.should.have.property('alerts').with.length(2);
});
it('should close via the alert', function() {
var alert = $rootScope.alerts[0];
alert.should.have.property('close');
alert.close();
$rootScope.should.have.property('alerts').with.length(1);
});
});
});
However, the beforeEach method is resetting the rootScope before each test (I kinda expected it to run before each "describe", not each "it"), so counting the number of alerts doesn't work.
What's the best way around this? Have multiple asserts within one big "it"? I'm quite new to unit testing in general and in Javascript in particular so any explanation is very welcome.
it's resetting the rootScope because you have it declared as a variable and not actually injected into the method...try passing in $rootScope and delete the var declaration of it.