How to test function within Angular click-handler - javascript

I'm currently unit-testing my Angular controllers and the only portion of my code coverage that is lagging behind are functions within click-handlers, and the statements within these functions.
As an example, function(cap)... states function not covered and playersService.setCap... states statement not covered in relation to the below click-handler:
vm.setCap = function(cap) {
playersService.setCap({playerId: playerId}, {limit: cap});
};
How would I go about testing a function like this, and the statement within it? I'm just looking for a basic test (expect(function).toHaveBeenCalled).

Alright to test this you would want to use a mock version of your playersService which you can then just inject into your controller.
describe("Controller: yourController", function () {
var mockResponse = {};
var mockService = {
setCap: function(playerId, limit){
mockResponse.playerId = playerId,
mockResponse.limit = limit
}
};
var mockParams = 'cap';
var $controller;
beforeEach(inject(function (_$controller_) {
$controller = _$controller_;
}))
it("Should call the service on click", function () {
spyOn(mockService, 'setCap').and.callThrough();
var testedController = $controller('yourController', { playersService:mockService });
testedController.setCap(mockParams);
expect(mockService.toHaveBeenCalled).toHaveBeenCalled();
expect(mockResponse.limit.limit).toBe(mockParams);
})
});
This will give you an example for both white and blackbox testing for the functionality.

Related

AngularJS and Jasmine: isolateScope() is undefined

after two days of researching and testing, I need to ask for help.
I am trying to test a directive using Jasmine, and I don't want to include the Karma engine.
In order to see the result of the test, I use the jasmine-html library, and the jasmine css.
I am able to test services easily, but when it comes to testing directive I am not able to access to the isolated scope. One thing to keep in mind is that I don't want to use controllers in my directive, but link functions.
(according to Angular doc, controller should be used only when you want to expose an API to other directives.)
I found multiple answers on StackOverflow, but none of them worked.
The last thing I tried is based on this answered question StackOverflow.
This is the test I am trying to replicate
describe('Wikis Directive Test Suite', function () {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function () {
html = '<wikis></wikis>';
inject(function ($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
elem= $compile(elem)($scope);
//scope = elem.isolateScope(); /*This is always undefined!*/
scope = elem.scope(); /* this doesn't have addWiki, only the empty wikis array */
$rootScope.$digest();
});
});
it('add Wiki should add a valid wiki URL to artist', function () {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
The "only" difference, is that I need to use Jasmine 2.4.1 and Angular 1.0.7. Unfortunately it seems like with these libraries the test doesn't work.
The thing is that isolateScope() is always undefined!
I've created a plunker to reproduce the problem.
https://plnkr.co/edit/PRt350VlASShg5oVeY88
Ok, I (actually one of my colleagues found it) what I was doing wrong!
This is the right code which works.
describe('Wikis Directive Test Suite', function () {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function () {
html = '<wikis></wikis>';
inject(function ($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
elem = $compile(elem)($scope);
$scope.$digest();
scope = elem.isolateScope();
});
});
it('add Wiki should add a valid wiki URL to artist',inject( function () {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
}));
});

How do you mock an angularjs $resource factory

I have a resource factory
angular.module('mean.clusters').factory('Clusters', ['$resource',
function($resource) {
return $resource('clusters/:clusterId/:action', {
clusterId: '#_id'
}, {
update: {method: 'PUT'},
status: {method: 'GET', params: {action:'status'}}
});
}]);
and a controller
angular.module('mean.clusters').controller('ClustersController', ['$scope',
'$location', 'Clusters',
function ($scope, $location, Clusters) {
$scope.create = function () {
var cluster = new Clusters();
cluster.$save(function (response) {
$location.path('clusters/' + response._id);
});
};
$scope.update = function () {
var cluster = $scope.cluster;
cluster.$update(function () {
$location.path('clusters/' + cluster._id);
});
};
$scope.find = function () {
Clusters.query(function (clusters) {
$scope.clusters = clusters;
});
};
}]);
I am writing my unit tests and every example I find is using some form of $httpBackend.expect to mock the response from the server, and I can do that just fine.
My problems is, when unit testing my controller functions I would like to mock the Clusters object. If I'm using $httpBackend.expect, and I introduce a bug in my factory every unit test in my controller will fail.
I would like to have my test of $scope.create test only $scope.create and not also my factory code.
I've tried adding a provider in the beforeEach(module('mean', function ($provide) { part of my tests but I cant seem to get it right.
I also tried
clusterSpy = function (properties){
for(var k in properties)
this[k]=properties[k];
};
clusterSpy.$save = jasmine.createSpy().and.callFake(function (cb) {
cb({_id: '1'});
});
and setting Clusters = clusterSpy; in the before(inject but in the create function, the spy gets lost with
Error: Expected a spy, but got Function.
I have been able to get a spy object to work for the cluster.$update type calls but then it fails at var cluster = new Clusters(); with a 'not a function' error.
I can create a function that works for var cluster = new Clusters(); but then fails for the cluster.$update type calls.
I'm probably mixing terms here but, is there a proper way to mock Clusters with spies on the functions or is there a good reason to just go with $httpBackend.expect?
Looks like I was close a few times but I think I have it figured out now.
The solution was the 'I also tried' part above but I was not returning the spy object from the function.
This works, it can be placed in either the beforeEach(module( or beforeEach(inject sections
Step 1: create the spy object with any functions you want to test and assign it to a variable that's accessible to your tests.
Step 2: make a function that returns the spy object.
Step 3: copy the properties of the spy object to the new function.
clusterSpy = jasmine.createSpyObj('Clusters', ['$save', 'update', 'status']);
clusterSpyFunc = function () {
return clusterSpy
};
for(var k in clusterSpy){
clusterSpyFunc[k]=clusterSpy[k];
}
Step 4: add it to the $controller in the beforeEach(inject section.
ClustersController = $controller('ClustersController', {
$scope: scope,
Clusters: clusterSpyFunc
});
inside your tests you can still add functionality to the methods using
clusterSpy.$save.and.callFake(function (cb) {
cb({_id: '1'});
});
then to check the spy values
expect(clusterSpy.$save).toHaveBeenCalled();
This solves both problems of new Clusters() and Clusters.query not being a function. And now I can unit test my controller with out a dependency on the resource factory.
Another way to mock the Clusters service is this:
describe('Cluster Controller', function() {
var location, scope, controller, MockClusters, passPromise, q;
var cluster = {_id : '1'};
beforeEach(function(){
// since we are outside of angular.js framework,
// we inject the angujar.js services that we need later on
inject(function($rootScope, $controller, $q) {
scope = $rootScope.$new();
controller = $controller;
q = $q;
});
// let's mock the location service
location = {path: jasmine.createSpy('path')};
// let's mock the Clusters service
var MockClusters = function(){};
// since MockClusters is a function object (not literal object)
// we'll need to use the "prototype" property
// for adding methods to the object
MockClusters.prototype.$save = function(success, error) {
var deferred = q.defer();
var promise = deferred.promise;
// since the Clusters controller expect the result to be
// sent back as a callback, we register the success and
// error callbacks with the promise
promise.then(success, error);
// conditionally resolve the promise so we can test
// both paths
if(passPromise){
deferred.resolve(cluster);
} else {
deferred.reject();
}
}
// import the module containing the Clusters controller
module('mean.clusters')
// create an instance of the controller we unit test
// using the services we mocked (except scope)
controller('ClustersController', {
$scope: scope,
$location: location,
Clusters: MockClusters
});
it('save completes successfully', function() {
passPromise = true;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
// we can call "toHaveBeenCalledWith" since we mocked "location.path" as a spy
expect(location.path).toHaveBeenCalledWith('clusters/' + cluster._id););
});
it('save doesn''t complete successfully', function() {
passPromise = false;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
expect(location.path).toHaveBeenCalledWith('/error'););
});
});
});

AngularJS: Reassigning an object vs. replacing it in a controller

I am building a subscriber/observer pattern for displaying data in realtime for my angular app.
The observer is built with a factory injected into the angular controller and whose role is to fetch data and update it. The basic code structure can he found in this fiddle: http://jsfiddle.net/ctrager/67QR7/3/
var myApp = angular.module('myApp', [])
.factory('MyFactory', [function () {
var Collection = {};
Collection.isLoaded = 0;
Collection.data = [1, 2];
Collection.username = "corey and eric";
Collection.update = function () {
Collection.data.push(new Date())
}
Collection.replace = function () {
// If you do Collection.data = []
// here you are doing the same thing
// as the empty collection bug. I can't
// tell you EXACTLY why this confuses angular
// but I'm 99% sure it's the same phenomenon
Collection.data = [new Date()]
}
Collection.replace_fixed = function () {
// This works
Collection.data.length = 0
Collection.data.push(new Date())
}
return Collection;
}])
function MyCtrl($scope, MyFactory) {
$scope.name = 'Eric';
$scope.items = MyFactory.data;
$scope.replace = function(){
console.log("replace")
MyFactory.replace()
//$scope.items = MyFactor.data;
}
$scope.replace_fixed = function(){
console.log("replace_fixed")
MyFactory.replace_fixed()
//$scope.items = MyFactor.data;
}
$scope.update = function(){
console.log("update")
MyFactory.update()
}
}
The factory (MyFactory) contains a collection (Collection.data). Any push (/splice) to that collection is reflected in the scope, but if I replace the entire collection (Collection.replace()) the change is no longer reflected in $scope. Any idea why?
This works:
http://jsfiddle.net/67QR7/4/
changed the thing stored on scope to be the factory instead of data. then the html repeat to do items.data.
So it looks like this is because you replaced the reference inside collection, but that doesn't change where $scope.items was pointing to.
So you are creating a reference to MyFactory.data from $scope.items. Angular puts a $watch on $scope.items and looks for changes. When you call MyFactory.replace, you change MyFactory.data, but $scope.items remains the same. So as far as your watch is concerned, nothing has happened.
You can fix this by using replace_fixed, or watch for changes to MyFactory.data. http://jsfiddle.net/KtB93/
$scope.MyFactory = MyFactory;
$scope.$watch("MyFactory.data", function(newData) {
console.log('myFactory.data changed');
$scope.items = newData;
});
Or alternatively (probably better), you can use a function as the watch expression so you don't have to plop MyFactory on the scope (http://jsfiddle.net/XAW54/1/):
$scope.$watch(function() {
return MyFactory.data;
}, function(newData) {
$scope.items = newData;
});

How to include/inject functions which use $scope into a controller in angularjs?

I am trying to include a library of functions, held in a factory, into a controller.
Similar to questions like this:
Creating common controller functions
My main controller looks like this:
recipeApp.controller('recipeController', function ($scope, groceryInterface, ...){
$scope.groceryList = [];
// ...etc...
/* trying to retrieve the functions here */
$scope.groceryFunc = groceryInterface; // would call ng-click="groceryFunc.addToList()" in main view
/* Also tried this:
$scope.addToList = groceryInterface.addToList();
$scope.clearList = groceryInterface.clearList();
$scope.add = groceryInterface.add();
$scope.addUp = groceryInterface.addUp(); */
}
Then, in another .js file, I have created the factory groceryInterface. I've injected this factory into the controller above.
Factory
recipeApp.factory('groceryInterface', function(){
var factory = {};
factory.addToList = function(recipe){
$scope.groceryList.push(recipe);
... etc....
}
factory.clearList = function() {
var last = $scope.prevIngredients.pop();
.... etc...
}
factory.add = function() {
$scope.ingredientsList[0].amount = $scope.ingredientsList[0].amount + 5;
}
factory.addUp = function(){
etc...
}
return factory;
});
But in my console I keep getting ReferenceError: $scope is not defined
at Object.factory.addToList, etc. Obviously I'm guessing this has to do with the fact that I'm using $scope in my functions within the factory. How do I resolve this? I notice that in many other examples I've looked at, nobody ever uses $scope within their external factory functions. I've tried injecting $scope as a parameter in my factory, but that plain out did not work. (e.g. recipeApp.factory('groceryInterface', function(){ )
Any help is truly appreciated!
Your factory can't access your $scope, since it's not in the same scope.
Try this instead:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.addToList = groceryInterface.addToList;
$scope.clearList = groceryInterface.clearList;
$scope.add = groceryInterface.add;
$scope.addUp = groceryInterface.addUp;
}
recipeApp.factory('groceryInterface', function () {
var factory = {};
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
factory.clearList = function() {
var last = this.prevIngredients.pop();
}
});
Alternatively, you can try using a more object oriented approach:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.groceryFunc = new groceryInterface($scope);
}
recipeApp.factory('groceryInterface', function () {
function Factory ($scope) {
this.$scope = $scope;
}
Factory.prototype.addToList = function (recipe) {
this.$scope.groceryList.push(recipe);
}
Factory.prototype.clearList = function() {
var last = this.$scope.prevIngredients.pop();
}
return Factory;
});
You cannot use $scope in a factory as it is not defined. Instead, in your factory functions change the properties of the object the factory is returning, e.g.
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
these will then get passed on to your $scope variable
$scope.addToList = groceryInterface.addToList;
// ... = groceryInterface.addToList(); would assign to `$scope.addToList` what is returned, instead of the function itself.
This isn't the exact answer for this question, but I had a similar issues that I solved by simply passing $scope as an argument to a function in my factory. So it won't be the normal $scope, but $scope at the time the function in the factory is called.
app.controller('AppController', function($scope, AppService) {
$scope.getList = function(){
$scope.url = '/someurl'
// call to service to make rest api call to get data
AppService.getList($scope).then(function(res) {
// do some stuff
});
}
});
app.factory('AppService', function($http, $q){
var AppService = {
getList: function($scope){
return $http.get($scope.url).then(function(res){
return res;
});
},
}
return AppService;
});

using the mock folder for karma test in yeoman angularjs

I have a angularjs application, which I generated with yeoman. In the karma.conf.js is a reference to test/mock/**/*.js. I have troubles to find out, how I use this folder. Currently I have a simple Service:
'use strict';
angular.module('tvcalApp')
.factory('Series', function ($resource) {
return $resource('/search/:search');
});
and a Test
'use strict';
var $httpBackend;
describe('Service: Series', function () {
// load the service's module
beforeEach(module('tvcalApp'));
// instantiate service
var Series;
beforeEach(inject(function (_Series_) {
Series = _Series_;
}));
beforeEach(inject(function ($injector) {
var url_get = '/search/The%20Simpsons';
var response_get = [{"seriesid": "71663"}];
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET(url_get).respond(response_get);
}));
it('should return a list if search for The Simpsons', function () {
var res = Series.query({search: 'The Simpsons'});
$httpBackend.flush();
expect(res[0].seriesid === 71663);
});
});
This is working. But I wonder If I could use the mock folder from the karma.conf.js for the mocking function. Is it possible to move the mock part into the mock folder and use it for all unit test?
I could not find any example or documentation for this folder. Can someone please point me to to an example or documentation how to use the mock folder.
Basically i have done something like this looking at angular-mocks.js:
Let's say may app is called ql. and i have a loginService that i want to mock:
mocks/servicesMock.js looks like this:
'use strict';
var ql = {};
ql.mock = {};
ql.mock.$loginServiceMockProvider = function() {
this.$get = function() {
var $service = {
login: function() { }
};
return $service;
};
};
angular.module('qlMock', ['ng']).provider({
$loginServiceMock: ql.mock.$loginServiceMockProvider
});
Then in my tests i can injeck $loginServiceMock:
'use strict';
describe('LoginController tests', function () {
// load the controller's module
beforeEach(module('ql'));
// load our mocks module
beforeEach(angular.mock.module('qlMock'));
var loginController,
loginServiceMock,
scope;
// Initialize the controller and a mock scope
// $loginSericeMock will be injected from serviceMocks.js file
beforeEach(inject(function ($controller, $rootScope, $loginServiceMock) {
scope = $rootScope.$new();
loginServiceMock = $loginServiceMock;
loginController = $controller('LoginController', {
$scope: scope,
loginService: loginServiceMock
});
}));
});
The example by #gerasalus is useful, but to answer the question:
mocks is just a folder to put your code in to keep your project organized and the code in tests short and to the point. By keeping all your mocks in one place, it is easier to reuse them in tests... copying them from one test to another would be bad practice from a DRY perspective.
So, for example, you might have a service called 'Foo'
app/service/foo.js
Then you might create a mock of that service, called 'FooMock'
test/mocks/service/foo.js
And then you would create tests and inject whatever mocks you need, as is shown in gerasulus's answer.

Categories