I'm trying to write a unit test to see if the 'getStudents()' provider function in my controller gets called if some properties are appropriately set. Notice the .success() callback:
$scope.update = function update() {
// omitted, just doing some checking...
// finally
else if (key.length === 3 || $scope.students.length === 0) {
StudentsProvider.getStudents($scope.keyword, $scope.selectedFilters).success(function(data) {
$scope.students = data;
});
}
};
My karma unit test looks like this:
describe("Students: Controllers", function () {
var $scope;
var ctrl;
beforeEach(module('studentsApp'));
describe("SearchCtrl", function () {
// Mock the provider
var mockStudentsProvider = {
getStudents: function getStudents() {
return [
{
Education: [],
Person: [{
ID: 1,
Name: "Testing McTestsson",
SSN: "1234567890",
Address: "Fakestreet 3", MobilePhone: "7777777"
}]
}
];
}
};
var StudentsProvider;
beforeEach(inject(function ($controller, $rootScope) {
$scope = $rootScope.$new();
ctrl = $controller('SearchCtrl', { $scope: $scope, StudentsProvider: mockStudentsProvider});
StudentsProvider = mockStudentsProvider;
}));
describe("Update", function () {
beforeEach(function () {
spyOn(StudentsProvider, 'getStudents');
});
it("should always call the provider with 3 letters", function () {
$scope.keyword = "axe";
$scope.update();
expect(StudentsProvider.getStudents).toHaveBeenCalled();
expect(StudentsProvider.getStudents).toHaveBeenCalledWith("axe", "");
});
});
});
});
When I run this, I get the following error:
TypeError: 'undefined' is not an object (evaluating 'StudentsProvider.getStudents($scope.keyword, $scope.selectedFilters).success')
and it's probably because I'm not mocking the .success() callback. How would I do that? Thanks in advance!
Replace this:
var mockStudentsProvider = {
getStudents: function getStudents() {
return [{
Education: [],
Person: [{
ID: 1,
Name: "Testing McTestsson",
SSN: "1234567890",
Address: "Fakestreet 3",
MobilePhone: "7777777"
}]
}];
}
};
with this:
var mockStudentsProvider = {
getStudents: function getStudents() {
var retVal = [{
Education: [],
Person: [{
ID: 1,
Name: "Testing McTestsson",
SSN: "1234567890",
Address: "Fakestreet 3",
MobilePhone: "7777777"
}]
}];
return {
success: function(fn) {
fn(retVal)
};
}
}
};
And replace this:
spyOn(StudentsProvider, 'getStudents');
with this:
spyOn(StudentsProvider, 'getStudents').andCallThrough();
When you do not use andCallThrough() or andCallFake() jasmine prevents execution of the method and returns null. Inside your update method you are calling null.success. This will fail. (http://jasmine.github.io/1.3/introduction.html)
In your mock method you need to change the return format--the real http method returns an object where success refers to a function which takes an input a callback function.
In your case, the callback function is:
function(data) {
$scope.students = data;
}
Related
(function() {
'use strict';
angular
.module('walletInformation')
.controller('WalletInformationController', WalletInformationController);
/* #ngInject */
function WalletInformationController(
$q,
$scope,
config,
logger,
session,
$interpolate,
flowstack,
profileService,
walletInformationFormlyService,
postMsg
) {
// jshint validthis: true
var vm = this;
var app = $scope.app;
var user = session.get('profile').profile || {};
vm.saveWalletInformation = saveWalletInformation;
vm.onClickCancel = onClickCancel;
vm.deleteWallet = deleteWallet;
vm.model = {
walletInformation : {
firstName: user.name.first,
lastName: user.name.last,
emailAddress: user.emailAddress,
phoneNumber: user.mobilePhone.phoneNumber,
keepMeUpdated: user.preferences.receiveEmailNotification
}
};
activate();
////////////////
/**
* #function activate
* #description init function for the controller
* Calls paymentCard to get the
* */
function activate() {
createFormFields();
logger.info('Activated the Wallet Information View.');
}
function createFormFields() {
vm.fields = walletInformationFormlyService.getFormlyFields($scope.app.text);
}
function saveWalletInformation() {
var updatedProfile = createLegacyProfileInformation();
profileService.updateProfileInformation(updatedProfile).then(onSuccess).catch(onError);
function onSuccess(response) {
session.set('profile', {
profile: response.profile
});
if (vm.model.walletInformation.currentPassword && vm.model.walletInformation.newPassword) {
changePasswordRequest();
} else {
flowstack.add('accountManagement');
flowstack.next();
}
}
function onError(error) {
logger.error('Verify save Wallet Information Error: ', error);
}
//changePassword
function changePasswordRequest() {
var updatedPassword = {
data: {
currentPassword: vm.model.walletInformation.currentPassword,
newPassword: vm.model.walletInformation.newPassword
}
};
profileService
.changePassword(updatedPassword)
.then(onChangePasswordSuccess, onChangePasswordError);
function onChangePasswordSuccess(response) {
flowstack.add('accountManagement');
flowstack.next();
logger.info('Change password request: ', response);
}
function onChangePasswordError(response) {
logger.error('Change Password Error: ', response);
}
}
}
function deleteWallet() {
var appText = app.text.walletInformation;
console.log(appText);
flowstack.add('confirmation');
flowstack.next({
data: {
title: appText.deleteWalletConfirmationTitle,
body: appText.deleteWalletConfirmationBody,
cancelButton: appText.deleteWalletConfirmationCancelButton,
confirmButton: appText.deleteWalletConfirmationConfirmButton,
onConfirm: function() {
profileService.deleteWallet().then(deleteProfileSuccess, deleteProfileFailure);
function deleteProfileSuccess(response) {
if (response.profile) {
logger.info('profile is successfully deleted.', response);
postMsg.send('closeSwitch');
}
}
function deleteProfileFailure(error) {
logger.error('something went wrong while deleting profile.', error);
}
},
onCancel: function() {
flowstack.add('walletInformation');
flowstack.next();
}
}
});
}
function createLegacyProfileInformation() {
var updatedProfile = user;
var formlyField = vm.model.walletInformation;
//Update the profile
updatedProfile.name = {
first: formlyField.firstName,
last: formlyField.lastName
};
updatedProfile.mobilePhone.phoneNumber = formlyField.phoneNumber;
updatedProfile.preferences.receiveEmailNotification = formlyField.keepMeUpdated;
return {
data: updatedProfile
};
}
function onClickCancel() {
flowstack.back();
}
}
})();
Coverage is saying that onSuccess of saveWalletInformation and changePasswordRequest isnt't covered but I'm not sure exactly how to test it, the only thing I've tested now is that the saveWalletInformation function is calling the profile service:
describe.only('WalletInformationController ---', function() {
var controller, scope;
var userProfile = {
profile: {
name: {
first: 'someonefirstname',
last: 'someoneslastname'
},
emailAddress: 'someone#something.com',
mobilePhone: {
countryCode: 'US+1',
phoneNumber: '123 212 2342'
},
preferences: {
receiveEmailNotification: true
}
}
};
beforeEach(function() {
bard.appModule('walletInformation');
bard.inject(
'$q',
'$controller',
'$rootScope',
'api',
'flowstack',
'logger',
'session',
'profileService'
);
session.set('profile', userProfile);
});
beforeEach(function() {
sandbox = sinon.sandbox.create();
scope = $rootScope.$new();
scope.app = {
text: {
global: {},
walletInformation: {
deleteWalletConfirmationTitle: 'Confirm Wallet Deletion?'
},
userInformation: {
emailValidation: 'Please enter valid email'
},
signin: {
rememberMeLabel: 'remember me'
}
}
};
controller = $controller('WalletInformationController', {
$scope: scope
});
loggerErrorStub = sandbox.stub(logger, 'error');
sandbox.stub(logger, 'info');
controller = $controller('WalletInformationController', {
$scope: scope
});
});
describe('saveWalletInformation method', function() {
beforeEach(function() {
// apiStub.restore();
sandbox.stub(profileService, 'updateProfileInformation', function() {
return $q.when(userProfile);
});
});
it('should saveWalletInformation successfully', function() {
controller.saveWalletInformation();
expect(profileService.updateProfileInformation).to.have.been.called;
});
it('should log error msg when saveWalletInformation call fails', function() {
profileService.updateProfileInformation.restore();
sandbox.stub(profileService, 'updateProfileInformation', function() {
return $q.reject();
});
controller.saveWalletInformation();
scope.$apply();
expect(loggerErrorStub).to.have.been.calledOnce;
});
it('should call changePasswordRequest when currentPassword and newPassword are set', function() {
sandbox.stub(profileService, 'changePassword', function() {
return $q.when({
success: true,
extensionPoint: null
});
});
sandbox.stub(flowstack, 'add');
sandbox.stub(flowstack, 'next');
controller.saveWalletInformation();
// scope.$apply();
// expect(flowstack.next).to.have.been.calledOnce;
// expect(flowstack.add).to.have.been.calledOnce;
expect(profileService.updateProfileInformation).to.have.been.called;
// expect(profileService.changePassword).to.have.been.called;
});
});
You can let the service call go a step further and instead of verifying that service function was called, as you do here:
expect(profileService.updateProfileInformation).to.have.been.called;
You can instead mock the response from the api call with $httpBackend:
httpBackend.expectGET('your/url/here').respond(200, {response object...});
or
httpBackend.expectGET('your/url/here').respond(500, {error object...});
Then you'll be hitting both the error and success functions when your tests run
I am trying to skip going to Firebase for my data in my tests, and return some simple results instead. I do not want to test that the Firebase code works, but that my factories work in returning data.
I have the following factories:
// Get Firebase Reference
angular.module('MyApp').factory('FireBaseData', function(FIREBASE) {
var FireBaseReference = new Firebase(FIREBASE.URL); // FIREBASE.URL = xxx.firebaseio.com
return FireBaseReference;
})
// Get the Firebase Array of Team Records
angular.module('MyApp').factory('AllTeams', ["FireBaseData", "$firebaseArray",
function(FireBaseData, $firebaseArray) {
return $firebaseArray(FireBaseData.child('Teams'));
}
]);
I have created mocks that replace the individual functions, and my tests will use these.
'use strict';
var $MockFirebaseArray = function(ArrayWithData) {
return ArrayWithData;
};
var $MockFirebaseObject = function(ObjectWithData) {
return ObjectWithData;
};
var MockFirebaseData = function() {
return {
child: function(StringValue) {
return "";
}
};
};
Tests with the mocks:
'use strict';
describe('Firebase Mocks', function() {
var TestArray = [
{ 'aString': 'alpha', 'aNumber': 1, 'aBoolean': false },
{ 'aString': 'bravo', 'aNumber': 2, 'aBoolean': true },
{ 'aString': 'charlie', 'aNumber': 3, 'aBoolean': true },
{ 'aString': 'delta', 'aNumber': 4, 'aBoolean': true },
{ 'aString': 'echo', 'aNumber': 5 }
];
describe('MockFirebaseData', function() {
var TestFirebase = MockFirebaseData();
it('should return empty text ("") from FireBaseData', function() {
assert.equal('', TestFirebase.child('SomeNode'));
});
});
describe('$MockFirebaseArray', function() {
it('should have the data array passed', function() {
var TestData = $MockFirebaseArray(TestArray);
assert.equal(TestArray.length, TestData.length);
});
});
describe('$MockFirebaseObject', function() {
it('should have the data object passed', function() {
var TestData = $MockFirebaseObject(TestArray[0]);
assert.equal(TestArray[0].length, TestData.length);
assert.deepEqual(TestArray[0], TestData);
});
});
});
This shows that the Mocks are working to return data, which is what I want to stay away from actually accessing Firebase. Now, when I try to use my factory in a test, I am getting errors.
Test the Factory:
describe('Teams Module', function() {
beforeEach(module('MyApp')); // Load My Application
describe('AllTeams Array', function() {
// Create Test Data
var TeamData = [
{ "Key": 1, "Name":"Team 1", "Logo": "Team1.jpg" },
{ "Key": 3, "Name":"Team 3", "Logo": "Team3.jpg" },
{ "Key": 2, "Name":"Team 2", "Logo": "Team2.jpg" },
];
beforeEach(function () {
module(function($provide) {
var MockData = MockFirebaseData();
$provide.value('FireBaseData', MockData);
$provide.value('$firebaseArray', $MockFirebaseArray(TeamData));
});
});
it('can get an instance of AllTeams factory', inject(function(AllTeams) {
assert.isDefined(AllTeams);
}));
});
});
Error returned:
PhantomJS 1.9.8 (Windows 7 0.0.0)
Teams Module
AllTeams Array
can get an instance of AllTeams factory FAILED
TypeError: '[object Object],[object Object],[object Object]' is not a function (evaluating '$firebaseArray(FireBaseData.child('Teams'))')
at app/Team.js:9
Instead of:
$provide.value('$firebaseArray', $MockFirebaseArray(TeamData));
try this:
$provide.value('$firebaseArray', $MockFirebaseArray);
I believe this is what you were intending to do in the first place. When injected, your factory will then be able to call $firebaseArray as a function.
I'm trying to test the following service and seem to be having trouble matching the mock response:
public getCustomerDetails(customerID:string): ng.IPromise<ICustomerDetails> {
return this.testService.getCustomerDetails(customerID).then((customerResponse:ICustomerResult) => {
var customer = customerResponse.customers;
var customerDetailsResult:ICustomerDetails = {
customerNumber: customer.displayCustomerNumber,
name: customer.name,
userId: customer.buId,
customerType: customer.type,
address: customer.displayAddress
};
return customerDetailsResult;
});
}
Here is the Jasmine code:
describe('CustomerService', () => {
var mockTestService: any = {};
var $q: ng.IQService;
var createCustomerDetailsService;
var customerDetailsService;
var createResolvedPromise;
var resolvePromises;
var serviceResponse = {
customerNumber: 'string',
name: 'name',
userId: 'buId',
customerType: 'type',
address: 'displayAddress'
};
var rootScope;
beforeEach(() => {
module('app.customer');
inject(( _$q_, $injector) => {
this.$q = _$q_;
rootScope = $injector.get('$rootScope');
createResolvedPromise = (result) => {
return this.$q.when(result);
};
resolvePromises = () => {
rootScope.$apply();
};
createCustomerDetailsService = () => {
return new app.customer.CustomerService(
mockRnsService);
};
});
});
it('WILL search by customer ID and return a customers details', () => {
var searchResponsePromise;
mockTestService.getCustomerDetails = jasmine.createSpy("getCustomerDetails").and.callFake(createResolvedPromise);
customerDetailsService = createCustomerDetailsService();
searchResponsePromise = customerDetailsService.getCustomerDetails('12345678');
resolvePromises();
expect(searchResponsePromise).toEqual(serviceResponse);
});
});
The error I'm getting is:
TypeError: 'undefined' is not an object (evaluating 'customer.displayCustomerNumber')
Can anyone tell me why I'm getting this error? Thanks for any help.
createResolvedPromise sets up getCustomerDetails to return a promise with the value of the parameter that you pass in - in this case '12345678'. This means that the value of customerResponse is '12345678'. So customer which takes it's value from customerResponse.customers is undefined as there is no property customers on a string. So the error happens as you are trying to evaluate customer.displayCustomerNumber because customer is undefined.
I have a ui-select field
{
key: 'data_id',
type: 'ui-select',
templateOptions: {
required: true,
label: 'Select label',
options: [],
valueProp: 'id',
labelProp: 'name'
},
controller: function($scope, DataService) {
DataService.getSelectData().then(function(response) {
$scope.to.options = response.data;
});
}
}
How can I access that inner controller in my unit tests and check that data loading for the select field actually works ?
UPDATE:
An example of a test could be as such:
var initializePageController = function() {
return $controller('PageCtrl', {
'$state': $state,
'$stateParams': $stateParams
});
};
var initializeSelectController = function(selectElement) {
return $controller(selectElement.controller, {
'$scope': $scope
});
};
Then test case looks like:
it('should be able to get list of data....', function() {
$scope.to = {};
var vm = initializePageController();
$httpBackend.expectGET(/\/api\/v1\/data...../).respond([
{id: 1, name: 'Data 1'},
{id: 2, name: 'Data 2'}
]);
initializeSelectController(vm.fields[1]);
$httpBackend.flush();
expect($scope.to.options.length).to.equal(2);
});
You could do it a few ways. One option would be to test the controller that contains this configuration. So, if you have the field configuration set to $scope.fields like so:
$scope.fields = [ { /* your field config you have above */ } ];
Then in your test you could do something like:
$controller($scope.fields[0].controller, { mockScope, mockDataService });
Then do your assertions.
I recently wrote some test for a type that uses ui-select. I actually create a formly-form and then run the tests there. I use the following helpers
function compileFormlyForm(){
var html = '<formly-form model="model" fields="fields"></formly-form>';
var element = compile(html)(scope, function (clonedElement) {
sandboxEl.html(clonedElement);
});
scope.$digest();
timeout.flush();
return element;
}
function getSelectController(fieldElement){
return fieldElement.find('.ui-select-container').controller('uiSelect');
}
function getSelectMultipleController(fieldElement){
return fieldElement.find('.ui-select-container').scope().$selectMultiple;
}
function triggerEntry(selectController, inputStr) {
selectController.search = inputStr;
scope.$digest();
try {
timeout.flush();
} catch(exception){
// there is no way to flush and not throw errors if there is nothing to flush.
}
}
// accepts either an element or a select controller
function triggerShowOptions(select){
var selectController = select;
if(angular.isElement(select)){
selectController = getSelectController(select);
}
selectController.activate();
scope.$digest();
}
An example of one of the tests
it('should call typeaheadMethod when the input value changes', function(){
scope.fields = [
{
key: 'selectOneThing',
type: 'singleSelect'
},
{
key: 'selectManyThings',
type: 'multipleSelect'
}
];
scope.model = {};
var formlyForm = compileFormlyForm();
var selects = formlyForm.find('.formly-field');
var singleSelectCtrl = getSelectController(selects.eq(0));
triggerEntry(singleSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(1);
var multiSelectCtrl = getSelectController(selects.eq(1));
triggerEntry(multiSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(2);
});
Part of this is includes the question of whether or not this is possible, but I am trying to make a factory value called currentUser, which will hold a single use from userService. I am trying to figure out how to make this interaction occur.
If my factories are as follows:
app.factory('currentUser', function() {
});
app.factory('userService', function() {
return {
users: [{
name: "John",
password: "12",
email: "test#example.com",
phone: "238-491-2138"
}, {
name: "Austin",
password: "potaoes",
email: "example#gmail.com",
phone: "138-490-1251"
}]
};
});
and I have a controller that does the following, is there a way to put currentuser = userService.users[i];. Or if this is a terrible way of doing it, how might I setup a way to keep track of a "current user"?
$scope.login = function() {
for (var i = 0; i < userService.users.length; i++) {
if (userService.users[i].email.toLowerCase() === $scope.credentials.email.toLowerCase()) {
if (userService.users[i].password === $scope.credentials.password) {
$scope.messageLogin = "Success!";
$timeout(function() {
$timeout(function() {
$location.path("/account");
}, 500)
$scope.loggedIn = true;
$scope.messageLogin = "Redirecting...";
// currentUser == userService.users[i];
}, 500)
} else {
$scope.messageLogin = "Incorrect login details";
}
return;
}
}
$scope.messageLogin = "Username does not exist";
};
Not sure if this is possible due to the fact that the factory seems to always have a return and never a get/set scenario. So if this is a bad use for Factory, how should I go about it?
You have a couple of options. You can make it part of the user service itself:
app.factory('userService', function() {
var currentUser;
return {
getCurrentUser: function() {
return currentUser;
},
setCurrentUser: function(user) {
currentUser = user;
},
users: [{
name: "John",
password: "12",
email: "test#example.com",
phone: "238-491-2138"
}, {
name: "Austin",
password: "potaoes",
email: "example#gmail.com",
phone: "138-490-1251"
}]
};
});
or you can store it in a separate object:
app.factory('currentUser', function() {
var currentUser;
return {
getCurrentUser: function() {
return currentUser;
},
setCurrentUser: function(user) {
currentUser = user;
}
};
});
Services/Factories in AngularJS are singletons, so you should build your application with the expectation that a service will always resolve to the same value.
That being said, your service is just a JavaScript object and its fields/properties are mutable. I don't see anything wrong with adding a field called "current" to your "userService", which is designed to contain a reference to the current user.