How to unit test a chained method using Jasmine - javascript

I'm having a problem unit testing the following method:
$scope.changeLocation = function (url) {
$location.path(url).search({ ref: "outline" });
};
I've written the following unit test that currently fails with this error (TypeError: Cannot read property 'search' of undefined):
var $locationMock = { path: function () { }, search: function () { } };
it('changeLocation should update location correctly', function () {
$controllerConstructor('CourseOutlineCtrl', { $scope: $scope, $location: $locationMock });
var url = "/url/";
spyOn($locationMock, "path");
spyOn($locationMock, "search");
$scope.changeLocation(url);
expect($locationMock.search).toHaveBeenCalledWith({ ref: "outline" });
expect($locationMock.path).toHaveBeenCalledWith(url);
});
If I change my function to the following, the test passes:
$scope.changeLocation = function (url) {
$location.path(url);
$location.search({ ref: "outline" });
};
How do I unit test this method when I'm using method chaining? Do I need to setup my $locationMock differently? For the life of me I cannot figure this out.

That is because your mock does not return location object to be able to chain through. Using Jasmine 2.0 you can change your mock to:
var $locationMock = { path: function () { return $locationMock; },
search: function () { return $locationMock; } };
and
spyOn($locationMock, "path").and.callThrough();
spyOn($locationMock, "search").and.callThrough(); //if you are chaining from search
or add:
spyOn($locationMock, "path").and.returnValue($locationMock);
spyOn($locationMock, "search").and.returnValue($locationMock); //if you are chaining from search
Or just create a spy object (less code):
var $locationMock = jasmine.createSpyObj('locationMock', ['path', 'search']);
and
$locationMock.path.and.returnValue($locationMock);
$locationMock.search.and.returnValue($locationMock); //if you are chaining from search

try :
spyOn($locationMock, "path").and.callThrough();
Else you'r calling search on a mock not $location

Related

Unit testing jQuery getJSON function errors with the fail method, using Jasmine and Karma

I've written a unit test for a function that calls the jQuery getJSON method. The only thing that I'm testing is that it is called with the expected URL. My unit test uses a Jasmine Spy to mock the API call. However, when I run my unit tests I get this error:
1) should make a request to the expected URL when running on localhost
test module getDataFromApi function
TypeError: Cannot read property 'fail' of undefined
In my unit test I've created a Jasmine Spy, which returns the done and fail methods. What am I doing wrong here?
Here is my unit test:
describe('getFundDataFromApi function', function () {
beforeEach(function () {
spyOn($, "getJSON").and.callFake(function () {
return {
done: function () {},
fail: function () {}
};
});
});
it('should make a request to the expected URL when running on localhost', function () {
var expectedUrl = '/assets/mock-data/mock-data.json';
module.getDataFromApi();
expect($.getJSON).toHaveBeenCalled();
expect($.getJSON).toHaveBeenCalledWith({url:expectedUrl});
});
});
Function I'm trying to test: getDataFromApi
getDataFromApi: function () {
var mod = this;
var url = this.settings.apiUrl;
$.getJSON({
url: url
})
.done(function (data) {
mod.processApiData(data);
})
.fail(function () {
mod.displayErrorMessage();
});
},
In your function getDataFromApi you are chaining the call of fail after done but, in the mocked version of done, it returns nothing(undefined), so, you get TypeError: Cannot read property 'fail' of undefined.
You can make the done function to return an object with a fail property that is a function.
beforeEach(function() {
spyOn($, "getJSON").and.callFake(function() {
return {
done: function() {
return { fail: function() {} };
}
};
});
});
Or, one line ES6 version
spyOn($, "getJSON").and.callFake(() => ({ done: () => ({fail: () => {}}) }));
Or, if you are planning to do more in your tests, like testing success or failed responses, probably returning a jQuery Deferred can help you
beforeEach(function() {
spyOn($, "getJSON").and.callFake(function() {
const deferred = $.Deferred();
deferred.resolve({'success': true});
return deferred;
});
});
Calling deferred.reject({'success': false}); will give you the chance to test for errors.
Hope it helps

How to spyOn a service to test it -- AngularJS/Jasmine

Have tried everything I've found on the internet to make this work with no success. Trying to test a function in my service, but according to my coverage I'm never accessing it. Any help would be greatly appreciated :)
Service:
'use strict';
angular.module('Service').service('configService', function(
$rootScope, $http) {
var configObj = null;
return {
getConfig: function() {
if (configObj != null) {
console.log("returning cached config");
return configObj;
}
else {
return $http.get('conf.json').then(function(res) {
$http.get(res.confLocation).then(function(
locationResponse) {
configObj = locationResponse;
$rootScope.configObj = configObj;
console.log($rootScope.configObj);
return configObj;
});
});
}
}
};
});
getConfig is never being accessed in the tests I've tried.
ServiceTests:
'use strict';
describe('Service: configService', function() {
// load the controller's module
beforeEach(module('Service'));
var configService, $httpBackend, results, tstLocation, tstRes;
var tstConfig = {
"confLocation": "local-dev-conf.json"
};
var tstConfigEmpty = {};
var tstConfigObjEmpty = {};
var tstConfigObj = {
"AWS": {
"region": "us-east-1",
"endpoint": "http://localhost:8133"
}
};
// Initialize the controller and a mock scope
beforeEach(inject(function(_configService_, _$httpBackend_) {
inject(function($rootScope) {
$rootScope.USERNAME = 'TESTER';
$rootScope.configObj = tstConfigObj;
$rootScope.locationResponse = tstLocation;
$rootScope.res = tstRes;
});
configService = _configService_;
$httpBackend = _$httpBackend_;
//Problem here??
spyOn(configService, 'getConfig').and.callFake(function() {
return {
then: function() {
return "something";
}
};
});
}));
it('should return a promise', function() {
expect(configService.getConfig().then).toBeDefined();
});
it('should test backend stuff', inject(function() {
results = configService.getConfig(tstConfig);
$httpBackend.expectGET('conf.json').respond(tstConfig);
$httpBackend.expectGET('local-dev-conf.json').respond(tstConfigObj);
$httpBackend.flush();
}));
//Thanks Miles
it('should check if it was called', inject(function() {
results = configService.getConfig().then();
expect(configService.getConfig).toHaveBeenCalled();
});
// console.log(results);
}));
it('should check for a null configObj', inject(function() {
results = configService.getConfig(tstConfigObjEmpty).then(function() {
expect(results).toBe(null);
});
// console.log(results);
// console.log(tstConfigObj);
}));
it('should check for a non-null configObj', inject(function() {
results = configService.getConfig(tstConfigObj).then(function() {
// Any string is accepted right now -- Why??
expect(results).toEqual("returning cached config");
expect(results).toBe("returning cached config");
expect(results).toBe("your mom"); // SHOULDN'T BE WORKING BUT DOES
expect(results).toEqual("Object{AWS: Object{region: 'us-east-1', endpoint: 'http://localhost:8133'}}");
expect(results).toBe("Object{AWS: Object{region: 'us-east-1', endpoint: 'http://localhost:8133'}}");
});
// console.log(results);
// console.log(tstConfigObj);
}));
it('should check for null file', inject(function() {
results = configService.getConfig(tstConfigEmpty).then(function() {
expect(results).toEqual(null);
expect(results).toBe(null);
});
}));
it('should test a valid file', inject(function() {
results = configService.getConfig(tstConfig).then(function() {
expect(results).not.toBe(null);
expect(results).toEqual("Object{confLocation: 'local-dev-conf.json'}");
})
});
I think I'm using spyOn wrong, or not accessing getConfig in my tests properly. Thoughts?
EDIT: Here is my code coverage
EDIT 2: Changed test 3 thanks to a problem found by Miles, still no update on test coverage though. Something is wrong with my spyOn logic as Amy pointed out. I shouldn't be using callFake it seems?
EDIT 3: Got it accessing the function now thanks to Miles. Had to change my spyOn to:
spyOn(configService, 'getConfig').and.callThrough();
then add the test case:
results = configService.getConfig(tstConfig).then();
expect(configService.getConfig).toHaveBeenCalled();
Coverage now (still needs work)
You're calling a fake instead of the function. So the logic inside of the function does not get called.
You have an issue here:
results = configService.getConfig(tstConfigObj).then(function() {
expect(results).toHaveBeenCalled();
expect(results).toHaveBeenCalledWith(tstConfigObj);
});
getConfig takes no parameters, and neither does then. Omitting these errors, results is assigned the string "something" from then. Even if the expect statements fire, you seem to be testing if a string has been called. Try this instead:
results = configService.getConfig().then();
expect(configService.getConfig).toHaveBeenCalled();
What version of Jasmine are you using? The and.callFake syntax was added in Jasmine 2.0. Maybe the test suite just needs to point to the new version.
Jasmine 1.3 Docs
Jasmine 2.0 Docs

How do I chain Intern Page Object function calls?

Following the Intern user guide, I wrote a simple page object:
define(function(require) {
function ListPage(remote) {
this.remote = remote;
}
ListPage.prototype = {
constructor: ListPage,
doSomething: function(value) {
return this.remote
.get(require.toUrl('http://localhost:5000/index.html'))
.findByCssSelector("[data-tag-test-id='element-of-interest']")
.click().end();
}
};
return ListPage;
});
In the test, I want to call doSomething twice in a row, like this:
define(function(require) {
var registerSuite = require('intern!object');
var ListPage = require('../support/pages/ListPage');
registerSuite(function() {
var listPage;
return {
name: 'test suite name',
setup: function() {
listPage = new ListPage(this.remote);
},
beforeEach: function() {
return listPage
.doSomething('Value 1')
.doSomething('Value 2');
},
'test function': function() {
// ...
}
};
});
});
However, when I run the test, I get this error:
TypeError: listPage.doSomething(...).doSomething is not a function
I tried some approaches described in this question, to no avail.
A better way to implement page objects with Intern is as helper functions rather than Command wrappers. Groups of related helper functions can then be used to create Page Object modules.
// A helper function can take config parameters and returns a function
// that will be used as a Command chain `then` callback.
function doSomething(value) {
return function () {
return this.parent
.findByCssSelector('whatever')
.click()
}
}
// ...
registerSuite(function () {
name: 'test suite',
'test function': function () {
return this.remote.get('page')
// In a Command chain, a call to the helper is the argument
// to a `then`
.then(doSomething('value 1'))
.then(doSomething('value 2'));
}
}

Jasmine and angular mocks : mocking a service that handles local storage

I have one service called wd$cache, that is basically a wrapper for localStorage.setItem and get.item.
Now I'm trying to test a controller that uses that service to achieve a certain result. The main problem is that I have an IF statement that gets triggered only if you have localstorage set already which is driving me nuts! (we are doing TDD here)
SERVICE
(function () {
angular
.module('hub')
.controller('promotionNotificationCtrl', promotionNotificationCtrl);
promotionNotificationCtrl.$inject = [
'hub$promotions',
'hub$client',
'wd$cache'
];
function promotionNotificationCtrl(
hub$promotions,
hub$client,
wd$cache) {
var vm = this;
activate();
//////////
function activate () {
hub$promotions.get(hub$client.brand, hub$client.subbrand).success(function (data) {
if (!wd$cache.get('hub$notification')) {
wd$cache.add('before', 123);
} else {
wd$cache.add('after', 321);
}
});
}
}
})();
TEST
describe('The promotion notification controller', function () {
var controller,
hub$client,
$httpBackend,
wd$cache,
mockData = [{
"foo": "bar"
},
{
"faa": "boo"
}];
beforeEach(module('hub'));
beforeEach(module('wired.core'));
beforeEach(module(function ($provide) {
hub$client = {
brand: 'bw',
subbrand: 'plus'
};
wd$cache = {
add: function () {
},
get: function () {
}
};
$provide.value('hub$client', hub$client);
$provide.value('wd$cache', wd$cache);
spyOn(wd$cache, 'add');
}));
beforeEach(inject(function ($controller, _$httpBackend_, _hub$promotions_) {
controller = $controller('promotionNotificationCtrl');
$httpBackend = _$httpBackend_;
hub$promotions = _hub$promotions_;
// request
$httpBackend.expectGET("/umbraco/api/promotions/get/?brand=bw&lang=en&subbrand=plus").respond(200, mockData);
$httpBackend.flush();
}));
it('should attempt to add a cache with a "before" key if no previous "hub$notification" cache was found', function () {
expect(wd$cache.add).toHaveBeenCalledWith('before', 123); //WORKING
})
it('should attempt to add a cache with a "after" key if a previous "hub$notification" cache was found', function () {
localStorage.setItem('hub$notification');
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123); // NOT WORKING
// CANT GET THROUGH THE IF STATEMENT
})
});
Basically I can never get to 'Test Cases' after BeforeEach block, whatever I do. I've tried everything, since mocking it to use actual storage.
Any ideas?
You can provide a mock implementation that is already filled with some data:
var cache = {};
beforeEach(module(function ($provide) {
// ...
wd$cache = {
add: function (key, value) {
cache[key] = value;
},
get: function (key) {
return cache[key];
}
};
// add initial data here or in the individual tests, e.g.
// ...
}));
To set up the cache properly for a specific testcase you can use the cache field like this:
cache['hub$notification'] = 'whatever value makes sense here';
Of course you can also do this in beforeEach.
Currently you are trying to do it like this:
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123);
This is problematic for two reasons:
You are not updating the cache because you are spying on the add method without .andCallThrough(). You should fix this (add .andCallThrough() after spy creation) otherwise updates from the controller will not affect the cache.
The spy records your call instead. You don't want this for setup code because it makes subsequent assertions more complicated.

Jasmine Testing Ternary Conditionals

Assume we have the following JavaScript code.
object = _.isUndefined(object) ? '' : aDifferentObject.property;
How would we be able to write a test for either scenarios in Jasmine?
Would it require two seperate describe's? Or would we be able to have a ternary conditional in the test itself?
Thanks!
Jeremy
I will use two separate describe like this
// System Under Test
function getObjectValue() {
return _.isUndefined(object) ? '' : aDifferentObject.property;
}
// Tests
describe('when object is undefined', function() {
it('should return empty string', function() {
expect(getObjectValue()).toBe('');
});
});
describe('when object is no undefined', function () {
it('should return property from different object', function () {
expect(getObjectValue()).toBe(property);
});
});
Consider the following case (Angular JS/ES6/Jasmine, Controller 'as' syntax)
Code:
Controller.toggleWidgetView = () => {
Controller.isFullScreenElement() ? Controller.goNormalScreen() : Controller.goFullScreen();
};
Test cases in Jasmine:
describe('.toggleWidgetView()', function() {
it('should call goNormalScreen method', function() {
spyOn(Controller, 'isFullScreenElement').and.callFake(function(){
return true;
});
spyOn(Controller, 'goNormalScreen').and.callThrough();
Controller.toggleWidgetView();
expect(Controller.goNormalScreen).toHaveBeenCalled();
});
it('should call goFullScreen method', function() {
spyOn(Controller, 'isFullScreenElement').and.callFake(function(){
return false;
});
spyOn(Controller, 'goFullScreen').and.callThrough();
Controller.toggleWidgetView();
expect(Controller.goFullScreen).toHaveBeenCalled();
});
});
Both the test cases passed.
Basically we are calling the 'toggleWidgetView' method twice and in each invocation, the condition changes (true/false) as it will in real world.

Categories

Resources