Using Jasmine to test object initialization within Angular controller - javascript

I have a controller that initializes an object upon initialization of the controller, and would like to test that it was called with the specific params that it is actually called with.
I know that I can test that $scope.autoSaveObj has certain properties that would tell me that it in fact initialized, but how would I spy on the initialization event itself?
Essentially, I want to spy on new autoSaveObj just like I would a method.
The main reason I want to test and spy on the object contructor is so that my karma-coverage plugin will show those lines as covered. It won't show the lines as covered if I just test the state of $scope.autoSaveObject after the initialization.
App.controller('ItemCtrl',[ '$scope', function($scope){
$scope.autoSaveObject = new autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
}]);

My guess is the code example is of a partial controller, because properties in the $scope are used which are not initialized here.
Since autoSaveObj is not defined anywhere, I assumed it is a global function. Consider moving this to a service or factory instead.
The following example shows how to
mock autoSaveObj
verify the call parameters, and
verify that the created instance is actually an instance of the correct type.
angular.module('myApp', []).
controller('ItemCtrl', function($scope, $window) {
// Use the injected $window object, so we don't rely on
// the environment and it can be mocked easily.
$scope.autoSaveObject = new $window.autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
});
describe('ItemCtrl', function() {
var $controller;
var $scope;
var $window;
var controller;
beforeEach(module('myApp', function($provide) {
$window = {
// Create an actual function that can be spied on.
// Using jasmine.createSpy won't allow to use it as a constructor.
autoSaveObj: function autoSaveObj() {}
};
// Provide the mock $window.
$provide.value('$window', $window);
}));
beforeEach(inject(function(_$controller_, $rootScope) {
$controller = _$controller_;
$scope = $rootScope.$new();
}));
it('should instantiate an autoSaveObj', function() {
spyOn($window, 'autoSaveObj');
// Initialize the controller in a function, so it is possible
// to do preparations.
initController();
// Do function call expectations as you would normally.
expect($window.autoSaveObj).toHaveBeenCalledWith(jasmine.objectContaining({
saveCallback: jasmine.any(Function),
delay: 2000
}));
// The autoSaveObject is an instance of autoSaveObj,
// because spyOn was used, not jasmine.createSpy.
expect($scope.autoSaveObject instanceof $window.autoSaveObj).toBe(true);
});
function initController() {
controller = $controller('ItemCtrl', {
$scope: $scope
});
}
});
<link href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.css" rel="stylesheet"/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-mocks.js"></script>

Related

Angular $scope function replacement

For an old angular app (version 1), I was asked to upload some data via it. I used Selenium to execute a javascript script that replaces the Angular app's $scope upload function to something I can work with.
ie
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc() { ... }
Unfortunately, the new function does not have access to the $scope and various local non $scope functions found within that library.
ie.
...uploadFunc() {
localNonScopeFunc // ERROR: localNonScopeFunc not defined
$scope // ERROR: $scope not defined
}
I was able to get access to $scope indirectly but I still can't access any local functions.
I'm pretty sure I just need to bind the controller's this to function to resolve both issues but not sure how...
How would I bind the replaced $scope function to the angular app?
Update 1:
// existing library
var someApp= angular.module('wApp', ['oc.lazyLoad', 'lookup','menu','prompt','service']);
someApp.controller('somecontroller', function ($scope, $timeout, $interval, $http, $ocLazyLoad, $rootScope, service)
{
$scope.uploadFunc = function() {
$scope.doSomething();
NonScopeLibraryFunc();
...bad blocking code
};
}
function NonScopeLibraryFunc() {
...
}
I have to change the uploadFunc code since its blocking functionality. So I try
// selenium JavaScriptExecutor
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
};
Neither $scope or NonScopeLibraryFunc() can be used. I was able to indirectly use $scope but calling NonScopeLibraryFunc is still a no go.
I also tried binding
const s = angular.element(document.querySelector('#somecontroller')).scope();
const newUploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
}.bind(s);
s.uploadFunc = newUploadFunc;
But it also does work.
Following example of overloading an angular scope function should give you the basics of what you need.
Where you might run into issues is with any arguments that might be passed into the scope function from the view
// Non angular code
document.querySelector('button').addEventListener('click', function() {
const someVar = 'Local var text';
// get angular scope
const angScope = angular.element(document.querySelector('#ang-app')).scope()
console.log('Remote access $scope.txt = ', angScope.txt);
// store reference to original scope function
const oldFunc = angScope.func
// overload original function
angScope.func = function(){
// modify scope variable with local value
angScope.txt = someVar;
// call original scope function
oldFunc();
// if modifying the original scope that needs to be changed in view use $.apply()
angScope.$apply()
}
angScope.func();
});
// Angular app
angular.module('myApp', [])
.controller('main', function($scope) {
$scope.txt = 'Scope text';
$scope.func = function(){
console.log('controller func() called')
$scope.log()
}
$scope.log = function(){
console.log('Scope txt:', $scope.txt);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<button>Trigger modified scope func</button>
<div id="ang-app" ng-app="myApp" ng-controller="main">
Angular display: {{txt}}
</div>

Unit Testing Angular 1.5 component that requires ngModel

To test angular 1.5 components, the docs recommend you use ngMock's $componentController instead of using $compile if you don't need to test any of the DOM.
However, my component uses ngModel which I need to pass into the locals for $componentController, but there is no way to programmatically get the ngModelController; the only way to test it is to actually $compile an element with it on it, as this issue is still open: https://github.com/angular/angular.js/issues/7720.
Is there any way to test my components controller without resorting to $compiling it? I also don't want to have to mock the ngModelController myself as its behavior is somewhat extensive and if my tests rely on a fake one rather than the real thing there is a chance newer versions of Angular could break it (though that probably isn't an issue given Angular 1 is being phased out).
tl;dr: Solution is in the third code block.
but there is no way to programmatically get the ngModelController
Not with that attitude. ;)
You can get it programmatically, just a little roundabout. The method of doing so is in the code for ngMock's $componentController service (paraphrased here); use $injector.get('ngModelDirective') to look it up, and the controller function will be attached to it as the controller property:
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
var directives = $injector.get(componentName + 'Directive');
// look for those directives that are components
var candidateDirectives = directives.filter(function(directiveInfo) {
// ...
});
// ...
// get the info of the component
var directiveInfo = candidateDirectives[0];
// create a scope if needed
locals = locals || {};
locals.$scope = locals.$scope || $rootScope.$new(true);
return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs);
};
}];
Though you need to supply the ngModelController locals for $element and $attrs when you instantiate it. The test spec for ngModel demonstrates exactly how to do this in its beforeEach call:
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
parentFormCtrl = {
$$setPending: jasmine.createSpy('$$setPending'),
$setValidity: jasmine.createSpy('$setValidity'),
$setDirty: jasmine.createSpy('$setDirty'),
$$clearControlValidity: noop
};
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
$attrs: attrs
});
//Assign the mocked parentFormCtrl to the model controller
ctrl.$$parentForm = parentFormCtrl;
}));
So, adapting that to what we need, we get a spec like this:
describe('Unit: myComponent', function () {
var $componentController,
$controller,
$injector,
$rootScope;
beforeEach(inject(function (_$componentController_, _$controller_, _$injector_, _$rootScope_) {
$componentController = _$componentController_;
$controller = _$controller_;
$injector = _$injector_;
$rootScope = _$rootScope_;
}));
it('should update its ngModel value accordingly', function () {
var ngModelController,
locals
ngModelInstance,
$ctrl;
locals = {
$scope: $rootScope.$new(),
//think this could be any element, honestly, but matching the component looks better
$element: angular.element('<my-component></my-component>'),
//the value of $attrs.ngModel is exactly what you'd put for ng-model in a template
$attrs: { ngModel: 'value' }
};
locals.$scope.value = null; //this is what'd get passed to ng-model in templates
ngModelController = $injector.get('ngModelDirective')[0].controller;
ngModelInstance = $controller(ngModelController, locals);
$ctrl = $componentController('myComponent', null, { ngModel: ngModelInstance });
$ctrl.doAThingToUpdateTheModel();
scope.$digest();
//Check against both the scope value and the $modelValue, use toBe and toEqual as needed.
expect(ngModelInstance.$modelValue).toBe('some expected value goes here');
expect(locals.$scope.value).toBe('some expected value goes here');
});
});
ADDENDUM: You can also simplify it further by instead injecting ngModelDirective in the beforeEach and setting a var in the describe block to contain the controller function, like you do with services like $controller.
describe('...', function () {
var ngModelController;
beforeEach(inject(function(_ngModelDirective_) {
ngModelController = _ngModelDirective_[0].controller;
}));
});

Spying on recursive Angular controller method

I have a recursive method that, if a flag is set, will call itself every five seconds. I'm trying to write a test that spies on the method, calls it, waits six seconds and then expects the method to have been called twice. My test fails, as the spy reports the method only being called once (the initial call).
I'm using the Angular style guide, so am attaching these methods to a placeholder for this. I suspect there may be an issue with scoping of the controller returned from angular-mocks $controller(), but I'm not sure—most people are attaching methods to $scope.
Without attaching methods to $scope, how can I create a spy to verify that my method has been called twice?
app.js:
'use strict';
angular
.module('MyApp', [
//...
]);
angular
.module('MyApp')
.controller('MyController', MyController);
MyController.$inject = [
//...
];
function MyController() {
var vm = this;
vm.callMyself = callMyself;
vm.flag = false;
function callMyself () {
console.log('callMyself');
if (vm.flag) {
console.log('callMyself & flag');
setTimeout(vm.callMyself, 5000);
}
}
}
appSpec.js:
describe('MyController', function () {
var $scope;
beforeEach(function () {
module('MyApp');
});
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
controllerInstance = $controller('MyController', {$scope: $scope});
}));
it('should call itself within 6 seconds if flag is true', function (done) {
controllerInstance.flag = true;
spyOn(controllerInstance, 'callMyself');
controllerInstance.callMyself();
setTimeout(function () {
expect(controllerInstance.callMyself).toHaveBeenCalledTimes(2);
done();
}, 6000);
}, 7000);
});
Working Plunker
You need to use .and.callThrough() to further execute the function that would call itself:
By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation.
spyOn(controllerInstance, 'callMyself').and.callThrough();
Tested in the plunker - it works.

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

Testing angular controller: when to create new scope?

When testing an angular controller, is it not always necessary to create a new scope by calling $rootScope.new()?
Here is my controller:
myControllers.controller("myCtrl1", ['$scope', function($scope) {
$scope.todos = [{"name": "Learn Angular"}, {"name": "Install Karma"}];
$scope.date = '1/1/2014';
}]);
And here is my passing test:
describe("controllers", function() {
var $scope, $rootScope, $controller;
beforeEach(function() {
module("myApp.controllers");
});
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
//scope = $rootScope.new() **When would you do this?**
}));
//Basic Controller
it("sets todos on scope", function() {
var scope = {}; //**Creating an empty scope object**
$controller("myCtrl1", {$scope : scope});
expect(scope.todos.length).toBe(3);
});
});
I was under the assumption that I need to create a new clean scope every time I test a controller but apparently I was wrong as the above test passes. Any explanations?
Thanks in advance!
In your case it is passing because your controller is only assigning values to the scope, so it can be any object. But for example if the controller had to listen to an event then the test would fail.
$scope.$on('datachange', function(event, args) {
// do something
})
In that case you would have to create a new scope to make your test pass.
It's because you're not using any $scope's methods, like $on or $watch or whatever. Also, to trigger watches in a test, you quite often need to use $scope.$digest(). None of those will work if you pass an empty object as a scope to a controller, of course.
Depends on situation.

Categories