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.
Related
I'm having trouble with a chained spec when using ngDescribe. The second spec never finishes and times out. If I comment out the first spec, the second spec completes successfully. So, I know each spec is valid and works.
Here is the whole test with both specs.
var baseNotes = window.__mocks__['base-data/notes'];
var baseUsers = window.__mocks__['base-data/users'];
var baseDepartments = window.__mocks__['base-data/departments'];
// Create object to describe all the mocks
var mock = {
app: {
// Mock out all required DB functionality
LocalDb: {
'service': {
dbs: [
{
notes: true
}
]
},
'getDbInfo': function($q) {
return $q.when()
},
'allDocs': function($q) {
return $q.when(baseNotes);
},
'putDoc': function($q) {
var note = {};
note._id = 'xyz123';
console.log("Here is the note now");
console.log(note);
return $q.when(note);
}
},
Users: {
getUsers: function($q) {
return $q.when(baseUsers)
}
},
Departments: {
getDepartments: function($q) {
return $q.when(baseDepartments);
}
}
}
};
ngDescribe
({
name: 'Process Generic Notes Requests',
modules: ['app'],
// Must include $rootScope so that deps.step() is available
inject: ['Notes', 'NoteFactory', '$rootScope'],
mock: mock,
tests: function(deps){
beforeEach(function() {
// Preload the service with notes
deps.Notes.notes = baseNotes;
});
it('should refresh notes from db', function(done) {
// remove the notes from the service
deps.Notes.notes = [];
expect(deps.Notes.notes.length).toEqual(0);
deps.Notes.getNotes(true).then(
function(result) {
expect(result.length).toEqual(3);
done();
}
);
deps.step();
});
it('should fetch notes from memory', function(done) {
deps.Notes.getNotes().then(
function(result) {
expect(result.length).toEqual(3);
done();
}
);
deps.step();
});
it('should get specific note', function(done) {
deps.Notes.getNote(baseNotes[0]._id).then(
function(result) {
expect(result._id).toEqual(baseNotes[0]._id);
done();
}
);
deps.step();
});
it('should get last note', function(done) {
deps.Notes.getLastNote().then(
function(result) {
expect(result._id).toEqual('1436880052000-34lekd');
done();
}
);
deps.step();
});
it('should summarize notes', function() {
var summary = deps.Notes.summarizeNotes();
expect(summary.total).toEqual(3);
expect(summary.finished).toEqual(2);
expect(summary.remaining).toEqual(1);
console.log("Done processing summarize at " + Date.now());
});
}
})({
name: 'Process Update Note Requests',
modules: ['app'],
// Must include $rootScope so that deps.step() is available
inject: ['Notes', 'NoteFactory', '$rootScope'],
mock: mock,
tests: function (deps) {
var newNote;
beforeEach(function () {
// Preload the service with notes
deps.Notes.notes = baseNotes;
// Create a new note
newNote = deps.NoteFactory.createNote();
});
it('should save a new note', function (done) {
console.log("Starting save a note at " + Date.now());
console.log("Here is the note about to be saved");
console.log(newNote);
deps.Notes.updateNote(newNote).then(
function (response) {
dump('Response from update note!');
dump(response);
expect(response._id).toEqual('xyz123');
done();
},
function (error) {
dump('Failed to update note!');
dump(error);
expect('good').toEqual('bad');
done();
}
);
deps.step();
});
}
});
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);
});
I have a AngularJS factory 'UserSrvc'. This is responsible for calling a RESTful back end to get and create user accounts using Restangular:
(function () {
'use strict';
angular
.module('myapp')
.factory('UserSrvc', UserSrvc);
function UserSrvc(Restangular) {
return {
getAllUsers: getAllUsers,
getUser: getUser,
saveUser: saveUser
};
/////////////////////
function getAllUsers(){
return Restangular.all('users').getList();
}
function getUser(user){
return Restangular.setFullResponse(true).one('users', user).get();
}
function saveUser(user) {
return Restangular.all('users').post(user);
}
};
})();
My User controller then has functions for initializing the data for loading in to Angular UI Grid as well as functions for saving a user and getting user data:
(function () {
'use strict';
var controllerId = 'UserCtrl';
// Define the controller on the module
// Inject the dependencies.
// Point to the controller definition function.
angular
.module('myapp')
.controller(controllerId, UserCtrl, ['UserSrvc', 'ngDialog', '$log', 'toaster']);
function UserCtrl(UserSrvc, ngDialog, $log, toaster){
// Using the 'Controller As' syntax, so we assign to the vm variable (for view model).
var vm = this;
var allUsers = [];
// Bindable properties and functions are placed on vm.
vm.activate = activate;
vm.allUsers = {};
vm.toggleForm = false;
vm.saveUser = saveUser;
vm.gridOptions = {
data: allUsers,
enableSorting: true,
enableColumnResizing: true,
enableGridMenu: true,
showGridFooter: true,
showColumnFooter: true,
enableFiltering: true,
columnDefs: [
{name: 'firstName', field: 'First'},
{name: 'lastName', field: 'Last'},
{name: 'login', field: 'Login'},
{name: 'email', field: 'Email'}
]
};
activate();
function activate() {
return getUsers().then(function() {
// User Controller is now activated
$log.info('UserCtrl activated');
});
}
function refreshUserTable() {
return UserSrvc.getAllUsers()
.then(function(data) {
// User table refresh
vm.gridOptions.data = data.data;
$log.info('User table data refreshed.', vm.gridOptions.data);
});
}
function getUsers() {
return UserSrvc.getAllUsers()
.then(function (data) {
$log.debug('data: ', data);
vm.gridOptions.data = data;
//allUsers = data;
$log.debug('allUsers: ', vm.gridOptions.data);
return vm.gridOptions.data;
},
function(response) {
$log.debug("Failed to get users, error with status code", response.status);
});
}
function saveUser(vm) {
var new_user = {
"user": {
"First": vm.user.firstname,
"Last": vm.user.surname,
"Login": vm.user.username,
"Password": vm.user.password,
"Email": vm.user.email
}
};
//$log.debug('The user to be saved: ', user);
return UserSrvc.saveUser(new_user)
.then(function (data) {
$log.debug('The user to be saved: ', new_user);
$log.debug('response: ', data);
// Refresh the table
refreshUserTable(vm);
// Reset the user form
resetForm();
// Close the form
vm.toggleForm = !vm.toggleForm;
// Success toast
toaster.pop("success","User saved", "User '" + new_user.user.Login + "' successfully created");
return data;
},
function(response) {
$log.debug("Failed to save user, error with status code", response.status);
toaster.pop("error", "Unable to save user", "Failed to save user, error with status code " + response.status);
});
}
}
})();
On the first call to UserSrvc.getAllUsers() in the getUsers() function the data parameter from the .then(function(data) returns an array like so:
[
{
"Last": "Jobs",
"Email": "test#example.com",
"Login": "jobs",
"id": 1,
"First": "Steve"
}
]
However, subsequent calls made by refreshUserTable() to the same UserSrvc.getAllUsers(), the data parameter from .then(function(data)) returns an object like so:
{
"data": [
{
"Last": "Jobs",
"Email": "test#example.com",
"Login": "jobs",
"id": 1,
"First": "Steve"
}
]
}
To get it to work I need to pull the data array from the data object by doing data.data.
Why is it that subsequent calls made by the refreshUserTable() return an object and not an array? My suspicion is that it has something to do with the way in which I'm using Restangular or is there something glaringly obvious I've missed?
Ideally I'd like to get rid of the refreshUserTable() function and just use the getAllUsers() to refresh the table.
you set setFullResponse to true which extend your response object. You confused because Restangular uses same property key with you data.
If you want to use full response specifically on one method just use withConfig method of Restangular.
Restangular.withConfig(function(RestangularConfigurer) {
RestangularConfigurer.setFullResponse(true);
});
In my controller, I have a function executed on ng-init:
// Find a list of Messages
$scope.find = function() {
$scope.messages = Messages.query();
};
That's the service behind the query():
'use strict';
//Messages service used to communicate Messages REST endpoints
angular.module('messages').factory('Messages', ['$resource',
function($resource) {
return $resource('messages/:messageId', { messageId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
That's how each messag looks (aka the model):
0: Resource
$$hashKey: "00O"
__v: 0
_id: "546fb196971ba6fd20c8db62"
body: "foobar"
created: "2014-11-21T21:41:42.814Z"
location: Object
lat: 50.827409075117785
lng: 4.318828582763672
Angular has a $scope.markers object in which we can push markers that have lat and lng properties.
I need to go through the $scope.messages, get all location.lat and location.lng values and put them in $scope.markers._id.lat , $scope.markers._id.lng ..
How can we achieve this? I used angular.forEach without getting anything logged:
// Find a list of Messages
$scope.find = function() {
$scope.messages = Messages.query();
console.log($scope.messages);
angular.forEach($scope.messages, function(i, location) {
console.log($scope.messages[i]);
});
};
To access the messages from your query you need to do this:
var messages = Messages.query(function() {
console.log(messages);
});
If that correctly returns your messages to your console, your query is ok and you could then add them to your $scope.markers object:
var messages = Messages.query(function() {
angular.forEach(messages, function(obj, key) {
$scope.markers[obj._id] = {
'lat': obj.location.lat,
'lng': obj.location.lng
};
});
});
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;
}