Inside a directive of mine called by data-type-ahead I have the following, in a series of events:
$scope.test = 5;
// Bind blur event and clear the value of
element.bind('blur', function(){
$scope.test = 0;
});
I have tried a multitude of things to use in a unit test to correctly test the functionality of this blur event however I have not been successful. I have seen mention of the function triggerHandler. Here is my attempt at the unit test:
//Inject $compile and $rootScope before each test.
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$scope.test = 5
html = angular.element('<input type="text" data-type-ahead/>');
//Apply $scope to directive html.
directive = $compile(html)($scope);
//Trigger digest cycle.
$scope.$digest();
}));
it('should trigger the events bound to the blur event on the directive', function() {
html.triggerHandler('blur')
expect($scope.test).toEqual(0);
});
However this is failing because $scope.test is remaining on 5. Is it the html element is incorrect, do I need another $digest or $apply for after I trigger the event?
You have 2 ways of getting this to work. The first is adding a timeout to your method (docs):
// somewhere above add -
var originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
// in your beforeEach
beforeEach(function(){
html.triggerHandler('blur');
setTimeout(function(){
done();
}, 1000);
});
it('should trigger the events bound to the blur event on the directive',
function() {
expect($scope.test).toEqual(0);
}
);
I believe this to be "less good" practice (bad or worse is too negative a word for testing - the moment you test, you are already better :)). In general, I try to avoid testing async, because eventually my methods (a.k.a. units) are sync inside.
The "better practice" would be to write the method that changes the value like this:
// in the directive's ctrl
this.changeValue = function changeValue{
$scope.test = 0;
}
// later on set the watcher
// Bind blur event and clear the value of
$element.bind('blur', this.changeValue);
And then test the method itself instead of testing it async. You could test the $element.bind (via the spyOn(element, 'bind')) if you like to see that your ctrl/link methods create the binding.
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
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 have a regular angular app with a directive. This directive contains an element with a ng-click="clickFunction()" call. All works well when I click that element. I now need to write a test for this click, making sure that this function was actually run when the element was clicked - this is what I'm having trouble with.
Here's a jsfiddle to illustrate my issue: http://jsfiddle.net/miphe/v0ged3vb/
The controller contains a function clickFunction() which should be called on click. The unit test should imitate a click on the directive's element and thus trigger the call to that function.
The clickFunction is mocked with sinonjs so that I can check whether it was called or not. That test fails, meaning there was no click.
What am I doing wrong here?
I've seen the answer to similar questions like Testing JavaScript Click Event with Sinon but I do not want to use full jQuery, and I believe I'm mocking (spying on) the correct function.
Here's the js from the fiddle above (for those who prefer to see it here):
angular.js, angular-mocks.js is loaded as well.
// App
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function($scope) {
$scope.person = 'Mr';
$scope.clickFunction = function() {
// Some important functionality
};
});
myApp.directive('pers', function() {
return {
restrict: 'E',
template: '<h2 ng-click="clickFunction()" ng-model="person">Person</h2>',
};
});
// Test suite
describe('Pers directive', function() {
var $scope, $controller, template = '<pers></pers>', compiled;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller, $compile) {
$scope = $rootScope.$new();
ctrl = $controller('MyCtrl', {$scope: $scope});
compiled = $compile(template)($scope);
// Do I need to run a $scope.$apply() here?
console.log($scope.$apply); // This is a function, apparently.
//$scope.$apply(); // But running it breaks this function.
}));
it('should render directive', function() {
el = compiled.find('h2');
expect(el.length).to.equal(1);
});
it('should run clickFunction() when clicked', function() {
el = compiled.find('h2');
sinon.spy($scope, 'clickFunction');
// Here's the problem! How can I trigger a click?
//el.trigger('click');
//el.triggerHandler('click');
expect($scope.clickFunction.calledOnce).to.be.true
});
});
// Run tests
mocha.run();
Turns out the problem was quite hidden.
Firstly the $scope.$digest and $scope.$apply functions broke the beforeEach function which ultimately led to the whole solution.
Solution
Do not mix angular versions.
In the first fiddle
angular.js version 1.3.0
angular-mocks.js version 1.1.5
In the solved fiddle
angular.js version 1.3.0
angular-mocks.js version 1.3.0
That was the whole problem, and gave me quite obscure errors.
Thanks to Foxandxss from the #AngularJS IRC channel on freenode.
The way to trigger events on the directive with jQlite was simply:
someElement.triggerHandler('click');
I'm struggling unit testing a controller that watches a couple variables. In my unit tests, I can't get the callback for the $watch function to be called, even when calling scope.$digest(). Seems like this should be pretty simple, but I'm having no luck.
Here's what I have in my controller:
angular.module('app')
.controller('ClassroomsCtrl', function ($scope, Classrooms) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
$scope.classrooms = Classrooms.search(ctrl.functions.search_params());
}
});
});
And here's my unit test:
angular.module('MockFactories',[]).
factory('Classrooms', function(){
return jasmine.createSpyObj('ClassroomsStub', [
'get','save','query','remove','delete','search', 'subjects', 'add_subject', 'remove_subject', 'set_subjects'
]);
});
describe('Controller: ClassroomsCtrl', function () {
var scope, Classrooms, controllerFactory, ctrl;
function createController() {
return controllerFactory('ClassroomsCtrl', {
$scope: scope,
Classrooms: Classrooms
});
}
// load the controller's module
beforeEach(module('app'));
beforeEach(module('MockFactories'));
beforeEach(inject(function($controller, $rootScope, _Classrooms_ ){
scope = $rootScope.$new();
Classrooms = _Classrooms_;
controllerFactory = $controller;
ctrl = createController();
}));
describe('Scope: classrooms', function(){
beforeEach(function(){
Classrooms.search.reset();
});
it('should call Classrooms.search when scope.subject_list changes', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(Classrooms.search).toHaveBeenCalled();
});
});
});
I've tried replacing all the scope.$digest() calls with scope.$apply() calls. I've tried calling them 3 or 4 times, but I can't get the callback of the $watch to get called.
Any thoughts as to what could be going on here?
UPDATE:
Here's an even simpler example, that doesn't deal with mocks, stubbing or injecting factories.
angular.module('edumatcherApp')
.controller('ClassroomsCtrl', function ($scope) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
And unit test:
it('should set scope.test', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.test).toBeDefined();
});
This fails too with "Expected undefined to be defined." and nothing is logged to the console.
UPDATE 2
2 more interesting things I noted.
It seems like one problem is that newValue and oldValue are the same when the callback is called. I logged both to the console, and they are both equal to []. So, for example, if I change my $watch function to look like this:
$scope.$watch('subject_list', function(newValue, oldValue){
console.log('testing');
$scope.test = 'test';
});
the test passes fine. Not sure why newValue and oldValue aren't getting set correctly.
If I change my $watch callback function to be a named function, and just check to see if the named function is ever called, that fails as well. For example, I can change my controller to this:
$scope.update_classrooms = function(newValue, oldValue){
$scope.test = 'testing';
console.log('test');
};
$scope.watch('subject_list', $scope.update_classrooms);
And change my test to this:
it('should call update_classrooms', function(){
spyOn(scope,'update_classrooms').andCallThrough();
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.update_classrooms).toHaveBeenCalled();
});
it fails with "Expected spy update_classrooms to have been called."
In this case, update_classrooms is definitely getting called, because 'test' gets logged to the console. I'm baffled.
I just ran into this problem within my own code base and the answer turned out to be that I needed a scope.$digest() call right after instantiating the controller. Within your tests you have to call scope.$digest() manually after each change in watched variables. This includes after the controller is constructed to record the initial watched value(s).
In addition, as Vitall specified in the comments, you need $watchCollection() for collections.
Changing this watch in your simple example resolves the issue:
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
to:
$scope.$watchCollection('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
I made a jsfiddle to demonstrate the difference:
http://jsfiddle.net/ydv8k4zy/ - original with failing test - fails due to using $watch, not $watchCollection
http://jsfiddle.net/ydv8k4zy/1/ - functional version
http://jsfiddle.net/ydv8k4zy/2/ - $watchCollection fails without the initial scope.$digest.
If you play around With console.log on the second failing item you'll see that the watch is called with the same value for old and new after the scope.$digest() (line 25).
The reason that this isn't testable is because the way that the functions are passed into the watchers vs the way that spyOn works. The spyOn method takes the scope object and replaces the original function on that object with a new one... but most likely you passed the whole method by reference into the $watch, that watch still has the old method. I created this non-AngularJS example of the same thing:
https://jsfiddle.net/jonhartmann/9bacozmg/
var sounds = {};
sounds.beep = function () {
alert('beep');
};
document.getElementById('x1').addEventListener('click', sounds.beep);
document.getElementById('x2').addEventListener('click', function () {
sounds.beep = function () {
alert('boop');
};
});
document.getElementById('x3').addEventListener('click', function () {
sounds.beep();
});
The difference between the x1 handler and the x3 handler is that in the first binding the method was passed in directly, in the second the beep/boop method just calls whatever method is on the object.
This is the same thing I ran into with my $watch - I'd put my methods on a "privateMethods" object, pass it out and do a spyOn(privateMethods, 'methodname'), but since my watcher was in the format of $scope.$watch('value', privateMethods.methodName) it was too late - the code would get executed by my spy wouldn't work. Switching to something like this skipped around the problem:
$scope.$watch('value', function () {
privateMethods.methodName.apply(null, Array.prototype.slice.call(arguments));
});
and then the very expected
spyOn(privateMethods, 'methodName')
expect(privateMethods.methodName).toHaveBeenCalled();
This works because you're no longer passing privateMethods.methodName by reference into the $watch, you're passing a function that in turn executes the "methodName" function on "privateMethods".
Your problem is that you need to call $scope.$watchCollection instead of plain $watch.
$watch will only respond to assignments (i.e scope.subject_list = ['new item']).
In addition to assignments, $watchCollection will respond to changes to lists (push/splice).
I'm decorating forms like this:
angular.module('Validation').directive('form', function() {
return {
restrict: 'E',
link: function(scope, element) {
var inputs = element[0].querySelectorAll('[name]');
element.on('submit', function() {
for (var i = 0; i < inputs.length; i++) {
angular.element(inputs[i]).triggerHandler('blur');
}
});
}
};
});
Now, I'm trying to test this directive:
describe('Directive: form', function() {
beforeEach(module('Validation'));
var $rootScope, $compile, scope, form, input, textarea;
function compileElement(elementHtml) {
scope = $rootScope.$new();
form = angular.element(elementHtml);
input = form.find('input');
textarea = form.find('textarea');
$compile(form)(scope);
scope.$digest();
}
beforeEach(inject(function(_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
compileElement('<form><input type="text" name="email"><textarea name="message"></textarea></form>');
}));
it('should trigger "blur" on all inputs when submitted', function() {
spyOn(input, 'trigger');
form.triggerHandler('submit');
expect(input.trigger).toHaveBeenCalled(); // Expected spy trigger to have been called.
});
});
But, the test fails.
What's the right Angular way to test this directive?
You have some problems:
1) input = form.find('input'); and angular.element(inputs[i]); are 2 different wrapper objects wrapping the same underlying DOM object.
2) You should create a spy on triggerHandler instead.
3) You're working directly with DOM which is difficult to unit-test.
An example of this is: angular.element(inputs[i]) is not injected so that we have difficulty faking it in our unit tests.
To ensure that point 1) returns the same object. We can fake the angular.element to return a pre-trained value which is the input = form.find('input');
//Jasmine 1.3: andCallFake
//Jasmine 2.0: and.callFake
angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
return input; //return the same object created by form.find('input');
});
Side note: As form is already an angularJs directive, to avoid conflicting with an already defined directive, you should create another directive and apply it to the form. Something like this:
<form mycustomdirective></form>
I'm not sure if this is necessary. Because we're spying on a global function (angular.element) which may be used in many places, we may need to save the previous function and restore it at the end of the test. Your complete source code looks like this:
it('should trigger "blur" on all inputs when submitted', function() {
var angularElement = angular.element; //save previous function
angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
return input;
});
spyOn(input, 'triggerHandler');
form.triggerHandler('submit');
angular.element = angularElement; //restore
expect(input.triggerHandler).toHaveBeenCalled(); // Expected spy trigger to have been called.
});
Running DEMO
This is probably something to do with raising the 'submit' event during the test.
The angular team have created a pretty funky class to help them do this it seems to cover a lot of edge cases - see https://github.com/angular/angular.js/blob/master/src/ngScenario/browserTrigger.js
While this helper is from ngScenario I use it in my unit tests to overcome problems raising some events in headless browsers such as PhantomJS.
I had to use this to test a very similar directive that performs an action when a form is submitted see the test here https://github.com/jonsamwell/angular-auto-validate/blob/master/tests/config/ngSubmitDecorator.spec.js (see line 38).
I had to use this as I am using a headless browser for development testing purposes. It seems that to trigger an event in this type of browser the element that is triggering the event has to be attached to the dom as well.
Also as the form directive is one that angular already has you should either decorate this directive or give this directive a new name. I would actually suggest you decorate the ngSubmit directive instead of the form directive as this is more gear towards submitting a form. I actually have a very good example of this as I did this in the validation module I have open sourced. This should give you a very good start.
The directive source is here
The directive tests are here
Try hooking into the blur event:
it('should trigger "blur" on all inputs when submitted', function() {
var blurCalled = false;
input.on('blur', function() { blurCalled = true; });
form.triggerHandler('submit');
expect(blurCalled).toBe(true);
});