AngularJS: testing a service with a persistent rootscope - angularjs

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.

Related

How to unit test / mock a $timeout call?

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

Jasmine (Karma) test failing with $rootScope.$watch

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');
});
});

angularjs + jasmine : testing focus in a service

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.

Unit Test on $state in function

I have a function that triggers to change page. How can I unit test this? I'm always getting a failed result saying:
Expected ' ' to be 'add'
So the current state name is still the home page
Function
$scope.goToAddVote = function(){
$state.go('add');
}
Unit Testing
it('should redirect index.html to add.html after click on button', inject(function($state) {
scope.goToAddVote();
$state.go('add');
expect($state.current.name).toBe('add');
}));
EDIT: Using Nilo's answer
var mockStateService = {
go: jasmine.createSpy('add')
};
it('should redirect index.html to add.html after click on button', inject(function($state) {
scope.goToAddVote();
$state.go('add');
expect(mockStateService.go).toHaveBeenCalledWith('add');
}));
Your unit test is failing because the state you are trying to recieve is never registered in the first place. Thats why the go function will have no effect which ends in your current state-name being ''
What you should do ist inject a mock of the $state service with a jasmine-spy as a replacement for the go-function. In your assertion you then expect this function to have been called.
This is way cleaner in terms of unit-testing, because you do not also test the capabilities of $state.
The mocked service would look something along the lines of this:
var mockStateService = {
go: jasmine.createSpy()
}
The assertion then should look like this:
expect(mockStateService.go).toHaveBeenCalledWith('add');
EDIT: Your result could also be caused by the fact mentioned by Pankaj Parkar. So you can also try that, but either why you should mock the service so you really only test the code you have written yourself.
EDIT2: You are not injecting the mocked service, but still using the original $state
var mockStateService,
myScope,
ctrl;
beforeEach(inject(function($controller, $rootScope) {
mockStateService = {
go: jasmine.createSpy()
}
myScope = $rootScope.$new();
ctrl = $controller('myController', {
$scope: myScope,
$state: mockStateService
});
}));
// tip: write better descriptions, what you are writing is not what's really happening
it('should redirect index.html to add.html after click on button', function() {
myScope.goToAddVote();
expect(mockStateService.go).toHaveBeenCalledWith('add');
});

How does the createSpy work in Angular + Jasmine?

I made a simple demo of a factory and I am trying to test this using jasmine. I am able to run the test but I am using the spyOn method. I would rather use jasmine.createSpy or jasmine.createSpyObj to do the same test. Could someone help me to refactor my code so that uses these methods instead in my example?
http://plnkr.co/edit/zdfYdtWbnQz22nEbl6V8?p=preview
describe('value check',function(){
var $scope,
ctrl,
fac;
beforeEach(function(){
module('app');
});
beforeEach(inject(function($rootScope,$controller,appfactory) {
$scope = $rootScope.$new();
ctrl = $controller('cntrl', {$scope: $scope});
fac=appfactory;
spyOn(fac, 'setValue');
fac.setValue('test abc');
}));
it('test true value',function(){
expect(true).toBeTruthy()
})
it('check message value',function(){
expect($scope.message).toEqual(fac.getValue())
})
it("tracks that the spy was called", function() {
expect(fac.setValue).toHaveBeenCalled();
});
it("tracks all the arguments of its calls", function() {
expect(fac.setValue).toHaveBeenCalledWith('test abc');
});
})
update
angular.module('app',[]).factory('appfactory',function(){
var data;
var obj={};
obj.getValue=getValue;
obj.setValue=setValue;
return obj;
function getValue(){
return data;
}
function setValue(datavalue){
data=datavalue;
}
}).controller('cntrl',function($scope,appfactory){
appfactory.setValue('test abc');
$scope.message=appfactory.getValue()
})
I have changed your plunkr:
spy = jasmine.createSpy('spy');
fac.setValue = spy;
Edit
In Jasmine, mocks are referred to as spies. There are two ways to
create a spy in Jasmine: spyOn() can only be used when the method
already exists on the object, whereas jasmine.createSpy() will return
a brand new function.
Found the information here. The link has a lot more information about creating spies.
As said in the comments, you have absolutely no need for spies to test such a service. If you had to write the documentation for your service: you would say:
setValue() allows storing a value. This value can then be retrieved by calling getValue().
And that's what you should test:
describe('appfactory service',function(){
var appfactory;
beforeEach(module('app'));
beforeEach(inject(function(_appfactory_) {
appfactory = _appfactory_;
}));
it('should store a value and give it back',function() {
var value = 'foo';
appfactory.setValue(value);
expect(appfactory.getValue()).toBe(value);
});
});
Also, your service is not a factory. A factory is an object that is used to create things. Your service doesn't create anything. It is registered in the angular module using a factory function. But the service itself is not a factory.

Resources