AngularJS - Passing a function to spyOn - javascript

SO I have this simple, watered-down app that returns the NATO alphabet and then does unit tests on it using mocks and promises.
HERE'S A LIVE DEMO
I'm trying to spyOn a function from my .service() of MainModel. In the controller, I have a deferred promise called natoAlphabet that successfully displays on the page.
At first, I was referencing getNato from the MainController, but I never set MainController.getNato to the MainModel.getNato.
So I added in the MainController:
this.getNato = MainModel.getNato;
And I get the error of: Expected spy getNato to have been called.
However, in the console log, if you do a console output of mockMainCtrl the controller being mocked inside the beforeEach near the top, you get Object {name: "Hello World!", getNato: Promise}
and then below inside the first it() test, the output is Object {name: "Hello World!"} however, if you expand that one, you get:
Object {name: "Hello World!"}
getNato: Promise
name: "Hello world!";
__proto__: Object
Whereas the one inside the beforeEach, you had getNato.
My error
My error happens when the Jasmine test runs and I get Expected spy getNato to have been called. from the line expect(mockMainCtrl.getNato).toHaveBeenCalled(); on theSpec.js.
So what am I doing wrong?
I don't think there is anything wrong with app.js because the page can successfully read the promise.
Appendix:
theSpec.js:
describe('Controller: MainCtrl', function() {
beforeEach(module('app'));
var $scope, $q, mockMainCtrl, $controller, scope, deferred;
beforeEach(inject(function($controller, _$rootScope_, _$q_, MainModel) {
$q = _$q_;
$scope = _$rootScope_.$new();
deferred = _$q_.defer();
mockMainCtrl = $controller('MainCtrl', {
$scope: $scope,
MainModel: MainModel
});
console.log(mockMainCtrl);
}));
it('spied and have been called', function() {
spyOn(mockMainCtrl, 'getNato');
console.log(mockMainCtrl);
expect(mockMainCtrl.getNato).toHaveBeenCalled();
});
it('Name from service, instantiated from controller, to be mocked correctly', inject(function() {
expect(mockMainCtrl.name)
.toEqual("Hello World!");
}));
it('Get [getNato] mocked deferred promise', function(mainCtrl) {
deferred.resolve([{ id: 1 }, { id: 2 }]);
$scope.$apply();
expect($scope.results).not.toBe(undefined);
expect($scope.results).toEqual(['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel', 'India']);
expect($scope.error).toBe(undefined);
});
});
app.js:
var app = angular.module('app', []);
app.service('MainModel', function($q) {
this.name = "Hello World!";
var getNato = function() {
var deferred = $q.defer();
var theNatoAlphabet = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel', 'India'];
deferred.resolve(theNatoAlphabet);
return deferred.promise;
};
this.getNato = getNato();
});
app.controller('MainCtrl', function($scope, MainModel) {
this.name = MainModel.name;
var self = this;
MainModel.getNato.then(function(data) {
self.natoAlphabet = data;
$scope.results = data;
}).catch(function() {
$scope.error = 'There has been an error!';
});
this.getNato = MainModel.getNato;
});

Take a look at - http://plnkr.co/edit/57ZA8BXscmdY6oDX5IOA?p=preview.
You'd want to 'spyOn' the dependency i.e. the 'MainModel' here and do it before the '$controller' construction as the 'promise' is getting resolved on the construction of the controller. Hope this helps.
Something like -
beforeEach(inject(function($controller, _$rootScope_, _MainModel_) {
scope = _$rootScope_.$new();
MainModel = _MainModel_;
spyOn(MainModel, 'getNato').andCallThrough();
mockMainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
it('spied and have been called', function() {
expect(MainModel.getNato).toHaveBeenCalled();
});

Related

AngularJS/Karma/Jasmine - Service call not returning value

I'm attempting to make a call to a Github API using a service injected into a component - and yes, I am using AngularJS 1.5.3.
In the unit test, I am not receiving back a value (the function does work when I run it in the browser). I'm not sure what I'm doing wrong and hopefully someone could point me in the right direction.
Here's the error:
main.component.js
(function(){
angular.module("app").component("mainComponent", {
templateUrl: "/templates/main.component.html",
controllerAs: "vm",
controller: function(APIFactory, UserFactory, $state){
const vm = this;
vm.searchGithub = function(){
APIFactory.getAPI(vm.searchText).then(function(res){
res.status !== 200 ? $state.go("404", {errorData: res.data }) : (
vm.User = new UserFactory.User(res.data),
$state.go("profile", {userData: vm.User})
);
})
.catch(function(err){
$state.go("fourOFour");
});
};
}
});
})();
main.component.spec.js
describe("Main Component", function(){
var mainComponent, APIFactory, UserFactory, $httpBackend, $q, $state, $rootScope;
const addy = "https://api.github.com/users/";
beforeEach(angular.mock.module("app"));
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
$rootScope.$new();
mainComponent = _$componentController_("mainComponent", { $scope : {} });
}));
describe("Checking if the searchGithub() worked correctly", function(){
var result;
beforeEach(function(){
spyOn(mainComponent, "searchGithub").and.callThrough();
spyOn(APIFactory, "getAPI").and.callThrough();
result = {};
});
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
expect(mainComponent.searchText).toBeDefined();
// RESPONSE_SUCCESS does exist, I've omitted it.
$httpBackend.whenGET(addy + mainComponent.searchText).respond(200, $q.when(RESPONSE_SUCCESS));
// This is where I expect something to work
APIFactory.getAPI(mainComponent.searchText).then(function(res){
result = res;
});
$httpBackend.flush();
expect(APIFactory.getAPI).toHaveBeenCalledWith(mainComponent.searchText);
expect(mainComponent.User).toBeDefined();
});
});
});
So this is what I came up with for a solution. If anybody wants to give me a better solution, I am up for ideas.
First I made two mocks and then I injected them into mainComponent, as well as a spy for my mocked APIFactoryMock.getAPI function:
const APIFactoryMock = {
getAPI: function(){}
};
const UserFactoryMock = {
User: function(data){
return {
login: data.login,
id: data.id,
avatar_url: data.avatar_url,
html_url: data.html_url,
followers: data.followers,
following: data.following,
public_repos: data.public_repos,
public_gists: data.public_gists,
created_at: data.created_at,
updated_at: data.updated_at,
name: data.name,
company: data.company,
blog: data.blog,
location: data.location,
bio: data.bio,
hireable: data.hireable,
email: data.email,
links: {
followers_url: data.followers_url,
following_url: data.following_url,
subscriptions_url: data.subscriptions_url,
repos_url: data.repos_url,
organizations_url: data.organizations_url
}
}
}
};
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
$rootScope.$new();
spyOn(APIFactoryMock, "getAPI").and.returnValue(RESPONSE_SUCCESS);
bindings = { APIFactory: APIFactoryMock, UserFactory: UserFactoryMock, $state: $state };
mainComponent = _$componentController_("mainComponent", { $scope : {} }, bindings);
}));
And then I wrote tests for the mocks:
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
expect(mainComponent.searchText).toBeDefined();
mainComponent.searchGithub(mainComponent.searchText);
$httpBackend.whenGET(addy + mainComponent.searchText).respond(200, $q.when(RESPONSE_SUCCESS));
$httpBackend.flush();
mainComponent.User = UserFactoryMock.User(RESPONSE_SUCCESS.data);
expect(mainComponent.searchGithub).toHaveBeenCalledWith(mainComponent.searchText);
expect(mainComponent.User).toBeDefined();
expect(mainComponent.User.id).toEqual(666);
});
In the above answer, you are manually making a call to UserFactoryMock.User in the testcase, which will create an user object.
But to correctly test the functionality, to should be checking for UserFactory.User to be called when call to APIFactory.getAPI is success (without making a call to UserFactory.User manually in the testcase.
I would suggest modifying your testcase to something like below:
describe("Main Component", function(){
var mainComponent, APIFactory, UserFactory, $httpBackend, $q, $state, $rootScope;
const addy = "https://api.github.com/users/";
beforeEach(angular.mock.module("app"));
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
var scope = $rootScope.$new();
var bindings = { APIFactory: APIFactory, UserFactory: UserFactory, $state: $state };
mainComponent = _$componentController_("mainComponent", { $scope : scope }, bindings);
}));
describe("Checking if the searchGithub() worked correctly", function(){
var result;
beforeEach(function(){
spyOn(mainComponent, "searchGithub").and.callThrough();
spyOn(APIFactory, "getAPI").and.callFake(function() {
var def = $q.defer();
def.resolve(RESPONSE_SUCCESS);
return def.promise;
});
spyOn(UserFactory, "User").and.callFake(function() {
var user = { id: 666, .... };
return user;
});
});
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
$rootScope.$apply();
expect(mainComponent.searchText).toBeDefined();
mainComponent.searchGithub(); // Call the same way as it works in the code actually.
$rootScope.$apply();
//No manual call to 'UserFactory.User' or 'APIFactory.getAPI'. The call to 'APIFactory.getAPI' is resolved/succeeds, hence a call to 'UserFactory.User' is made and the same is tested
expect(APIFactory.getAPI).toHaveBeenCalledWith(mainComponent.searchText);
expect(UserFactory.User).toHaveBeenCalledWith(RESPONSE_SUCCESS.data);
expect(mainComponent.User).toBeDefined();
expect(mainComponent.User.id).toEqual(666);
});
});
});

How to test that my service returns data with jasmine and angularjs

I've begun using jasmine to test my controllers in angularjs however after reading some tutorials I am a bit stuck.
I have this simple angularjs controller called jasmineController
(function () {
"use strict";
var myAppModule = angular.module('myApp');
myAppModule.controller('jasmineController', ['$scope', 'genericService',
function ($scope, genericService) {
$scope.name = 'Superhero';
$scope.counter = 0;
$scope.$watch('name', function (newValue, oldValue) {
$scope.counter = $scope.counter + 1;
});
$scope.testPromise = function() {
return genericService.getAll("dashboard", "currentnews", null, null, null);
}
$scope.getNewsItems = function () {
genericService.getAll("dashboard", "currentnews", null, null, null).then(function (data) {
$scope.name = 'Superhero';
$scope.newsItems = data;
});
}
}
]);
})();
In my jasmine test I wanted to call getNewsItems and check that it can call genericService.getAll and that $scope.newsItems is assigned some data. I understand that I would be mocking out the service and I won't actually call it.
Here is my spec
describe("test", function () {
// Declare some variables required for my test
var controller, scope, genericService;
// load in module
beforeEach(module("myApp"));
beforeEach(inject(function ($rootScope, $controller, _genericService_) {
genericService = _genericService_;
// assign new scope to variable
scope = $rootScope.$new();
controller = $controller('jasmineController', {
'$scope': scope
});
}));
it('sets the name', function () {
expect(scope.name).toBe('Superhero');
});
it('should assign data to scope', function() {
//var fakeHttpPromise = {success: function () { }};
scope.getNewsItems();
spyOn(genericService, 'getAll');
expect(genericService.getAll).toHaveBeenCalledWith('dashboard', 'currentnews');
});
});
I've got a spyon for genericService.getall() but apart from that I am a bit stuck with checking that my scope variable is assigned a value.
I also get this stack trace:
Error: Expected spy getAll to have been called with [ 'dashboard', 'currentnews' ] but it was never called.
at stack (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1441:11)
at buildExpectationResult (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1408:5)
at expectationResultFactory (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:533:11)
at Spec.prototype.addExpectationResult (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:293:5)
at addExpectationResult (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:477:9)
at Anonymous function (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1365:7)
at Anonymous function (file:///C:/Projects/2013/AMT2015/AMT2015.WebAPP/Scripts/tests/controllers/dashboardControllerSpec.js:49:9)
at attemptSync (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1759:9)
at QueueRunner.prototype.run (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1747:9)
at QueueRunner.prototype.execute (file:///C:/Users/nickgowdy/Local%20Settings/Application%20Data/Microsoft/VisualStudio/12.0/Extensions/4sg2jkkc.gb4/TestFiles/jasmine/v2/jasmine.js:1733:5)
You need to put the spy first before calling the test function. And you are you actually passing more parameters to the service function. So you need to test with the exact parameter list.
it('should assign data to scope', function() {
//var fakeHttpPromise = {success: function () { }};
spyOn(genericService, 'getAll');
scope.getNewsItems();
expect(genericService.getAll).toHaveBeenCalledWith('dashboard', 'currentnews',null,null,null);
});
I ended up doing this:
describe("test", function () {
// Declare some variables required for my test
var controller, scope, genericService;
// load in module
beforeEach(module("myApp"));
beforeEach(inject(function ($rootScope, $controller, _$q_, _genericService_) {
genericService = _genericService_;
var deferred = _$q_.defer();
deferred.resolve('resolveData');
spyOn(genericService, 'getAll').and.returnValue(deferred.promise);
scope = $rootScope.$new();
controller = $controller('jasmineController', {
'$scope': scope
});
}));
it('sets the name', function () {
expect(scope.name).toBe('Superhero');
});
it('should assign data to scope', function() {
//spyOn(genericService, 'getAll').and.callFake(function() {
//});
scope.getNewsItems();
scope.$apply();
expect(scope.newsItems).toBe('resolveData');
//expect(genericService.getAll).toHaveBeenCalledWith('dashboard', 'currentnews', null, null, null);
});
});
Because my test is more than just calling a service but handling a promise as well I had to inject $q. Then with spy on I say to call service and method and the return value is the deferred promise.
Finally I can look at the scope variable to see if anything is assigned with this line:
expect(scope.newsItems).toBe('resolveData');
Thanks to everyone that helped.

How to spy on anonymous function using Jasmine

I'm using Jasmine to test my angular application and want to spy on an anonymous function.
Using angular-notify service https://github.com/cgross/angular-notify, I want to know whether notify function have been called or not.
Here is my controller:
angular.module('module').controller('MyCtrl', function($scope, MyService, notify) {
$scope.isValid = function(obj) {
if (!MyService.isNameValid(obj.name)) {
notify({ message:'Name not valid', classes: ['alert'] });
return false;
}
}
});
And here is my test:
'use strict';
describe('Test MyCtrl', function () {
var scope, $location, createController, controller, notify;
beforeEach(module('module'));
beforeEach(inject(function ($rootScope, $controller, _$location_, _notify_) {
$location = _$location_;
scope = $rootScope.$new();
notify = _notify_;
notify = jasmine.createSpy('spy').andReturn('test');
createController = function() {
return $controller('MyCtrl', {
'$scope': scope
});
};
}));
it('should call notify', function() {
spyOn(notify);
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();
});
});
An obviously return :
Error: No method name supplied on 'spyOn(notify)'
Because it should be something like spyOn(notify, 'method'), but as it's an anonymous function, it doesn't have any method.
Thanks for your help.
Daniel Smink's answer is correct, but note that the syntax has changed for Jasmine 2.0.
notify = jasmine.createSpy().and.callFake(function() {
return false;
});
I also found it useful to just directly return a response if you only need a simple implementation
notify = jasmine.createSpy().and.returnValue(false);
You could chain your spy with andCallFake see:
http://jasmine.github.io/1.3/introduction.html#section-Spies:_andCallFake
//create a spy and define it to change notify
notify = jasmine.createSpy().andCallFake(function() {
return false;
});
it('should be a function', function() {
expect(typeof notify).toBe('function');
});
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();

How to test my http request in my app

I am trying to write units test for my app and I have the following issue
In my controller, I have something like
$scope.test1 = function() {
productFactory.getName()
.then(function(products){
$scope.result = products;
})
}
productFactory
angular.module('myApp').factory('productFactory', function($http) {
var factoryObj = {};
factoryObj.getName = function() {
return http.get(url)
}
return factoryObj
})
In my unit test file
describe('test here', function () {
var testCtrl, scope, httpBackend, mockFactory;
beforeEach(module('myApp', function($provide){
$provide.value('productFactory', mockFactory);
}));
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
it('should get product name', function() {
scope.test1();
//I am not sure how to test the results
});
}));
When I run karma test, it gives me
TypeError: 'undefined' is not an object (evaluating 'productFactory.getName()')
I am not sure how to test the http result and fix the error. Can anyone help me about it? Thanks a lot!
First of all, you don't need to worry about using $provide:
beforeEach(module('myApp'));
1. Without $httpBackend (mock out the service completely)
Then, productFactory will be passed into your controller, but you want to spyOn the getName():
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
// add spy for the method, wrap with $q.when so it returns a promise
spyOn(mockFactory, 'getName').and.returnValue($q.when('Pizza!'));
testCtrl = _$controller_('testCtrl', {
$scope: scope,
productFactory: mockFactory // pass in here
});
Then, you've got to cause a $digest cycle, so that the promise will call through:
it('should get product name', function() {
scope.test1();
// hit the $digest
scope.$apply();
// expectation
expect(scope.result).toBe('Pizza!')
});
2. With $httpBackend
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
// set up httpBackent
httpBackend.when('GET', '/products')
.respond([{ name: 'Pizza!'}, {name: 'Sandwich'}]);
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
We don't need to mock the factory in this case at all. Then, we just need to flush $httpBackend when we want the http call to return:
it('should get product name', function() {
scope.test1();
// hit the $digest with flush
httpBackend.flush();
// expectation
expect(scope.result.length).toBe(2)
});

$scopeProvider <- $scope/ Unknown provider

I testing my angular-application with jasmine(http://jasmine.github.io/2.0/) and getting next error:
Unknown provider: $scopeProvider <- $scope
I know, that it's incorrect to build dependency with scope in filters, services, factories, etc., but I use $scope in controller!
Why am i getting this error? controller looks like
testModule.controller('TestCont', ['$filter', '$scope', function($filter, $scope){
var doPrivateShit = function(){
console.log(10);
};
this.lol = function(){
doPrivateShit();
};
this.add = function(a, b){
return a+b;
};
this.upper = function(a){
return $filter('uppercase')(a);
}
$scope.a = this.add(1,2);
$scope.test = 10;
$scope.search = {
};
}]);
and my test's code:
'use strict';
describe('testModule module', function(){
beforeEach(function(){
module('testModule');
});
it('should uppercase correctly', inject(function($controller){
var testCont = $controller('TestCont');
expect(testCont.upper('lol')).toEqual('LOL');
expect(testCont.upper('jumpEr')).toEqual('JUMPER');
expect(testCont.upper('123azaza')).toEqual('123AZAZA');
expect(testCont.upper('111')).toEqual('111');
}));
});
You need to manually pass in a $scope to your controller:
describe('testModule module', function() {
beforeEach(module('testModule'));
describe('test controller', function() {
var scope, testCont;
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
testCont = $controller('TestCont', {$scope: scope});
}));
it('should uppercase correctly', function() {
expect(testCont.upper('lol')).toEqual('LOL');
expect(testCont.upper('jumpEr')).toEqual('JUMPER');
...
});
});
});
Normally, a $scope will be available as an injectable param only when the controller is attached to the DOM.
You need to associate somehow the controller to the DOM (I'm mot familiar with jasmine at all).
I am following a video tutorial from egghead (link bellow) which suggest this approach:
describe("hello world", function () {
var appCtrl;
beforeEach(module("app"))
beforeEach(inject(function ($controller) {
appCtrl = $controller("AppCtrl");
}))
describe("AppCtrl", function () {
it("should have a message of hello", function () {
expect(appCtrl.message).toBe("Hello")
})
})
})
Controller:
var app = angular.module("app", []);
app.controller("AppCtrl", function () {
this.message = "Hello";
});
I am posting it because in the answer selected we are creating a new scope. This means we cannot test the controller's scope vars, no?
link to video tutorial (1min) :
https://egghead.io/lessons/angularjs-testing-a-controller

Categories