i'm currently just trying to test if getTodaysHours function on my controller has been called. ultimately the function should get hours from the mock JSON data and pass if parameters match, but i'm stuck on the first part.
vendor.controller
export class VendorController {
constructor($rootScope, data, event, toastr, moment, _, distanceService, vendorDataService, userDataService, stateManagerService) {
'ngInject';
//deps
this.$rootScope = $rootScope;
this.toastr = toastr;
this._ = _;
this.userDataService = userDataService;
this.vendorDataService = vendorDataService;
this.stateManagerService = stateManagerService;
this.event = event;
//bootstrap
data.isDeepLink = true;
this.data = data;
this.data.last_update = moment(this.data.updated_at).format('MM/DD/YY h:mm A');
this.data.distance = distanceService.getDistance(this.data.loc.lng, this.data.loc.lat);
this.data.todaysHours = this.getTodaysHours();
this.data.rating_num = Math.floor(data.rating);
this.hasReviewed = (userDataService.user.reviewed[data._id]) ? true : false;
this.isGrid = false;
this.isSearching = false;
this.hideIntro = true;
this.menuCollapsed = true;
this.filterMenuCollapsed = true;
this.selectedCategory = 'All';
this.todaysHours = '';
this.type = '';
this.searchString = '';
this.reviewScore = 0;
this.today = new Date().getDay();
this.vendorDataService.currentVendor = data;
//load marker onto map
$rootScope.$broadcast(event.ui.vendor.pageLoad, data);
//get menu
vendorDataService.getVendorMenu(data._id)
.then((res)=> {
this.data.menu = res.menu;
this.menuContainer = this.data.menu;
this.totalResults = this.getTotalResults();
this.availableMenuCategories = this.getAvailableMenuCategories();
})
.catch(() => {
this.toastr.error('Whoops, Something went wrong! We were not able to load the menu.', 'Error');
});
}
//get todays hours
getTodaysHours() {
let today = this.data.hours[new Date().getDay()];
return (today.opening_time || '9:00am') + ' - ' + (today.closing_time || '5:00pm');
}
}
the first test passes when I mock the JSON data with $provide constant
describe('vendor controller', () => {
let vm,
data = {"_id":"56b54f9368e685ca04aa0b87","lat_lon":"33.713018,-117.841101","hours":[{"day_of_the_week":"sun","closing_time":" 7:00pm","opening_time":"11:00am","day_order":0,"id":48880},...];
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
//first test
it('should pass', () => {
expect(data._id).toEqual('56b54f9368e685ca04aa0b87');
});
//second test
it('should call getTodaysHours', () => {
expect(vm.getTodaysHours()).toHaveBeenCalled();
});
});
then I tried to inject the controller (not sure if correct syntax):
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
beforeEach(inject(($controller) => {
vm = $controller('VendorController');
spyOn(vm,'getTodaysHours').and.callThrough();
}));
and it gives me some kind of forEach error. the second test gives me a undefined error when evaluating vm.getTodaysHours():
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should pass FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should call getTodaysHours FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
TypeError: undefined is not an object (evaluating 'vm.getTodaysHours') in /Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js (line 9)
/Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js:9:244419
You need to inject the dependencies of your controller when instantiating it with $controller. For example, consider the following controller:
class MyController {
constructor($rootScope, $log) {
// Store the controllers dependencies
this.$rootScope = $rootScope;
this.$log = $log;
}
// Return obituary value from the $rootScope
getValue() {
this.$log.debug('Retrieving value');
return this.$rootScope.foobar;
}
// Get the current date
getDate() {
this.$log.debug('Getting date');
return Date.now()
}
static get $inject() {
return ['$scope', '$log'];
}
}
I've written this controller using ES6, note that the dependencies are defined within the static $injectgetter at the foot of the class declaration. This will be picked up by AngularJS upon instantiation.
As you can see, the controller depends upon the $rootScope and the $log provider. When mocking this controller for testing purposes, you must inject the controllers dependencies like this:
describe('Spec: MyController', () => {
var controller;
beforeEach(inject(($rootScope, $log, $controller) => {
controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', () => {
var value = controller.getValue();
// ... perform checks
});
it('should return the current date', () => {
var date = controller.getDate();
// ... perform checks
});
});
More recent versions of Jasmine enable developers to leverage the this keyword throughout their tests.
Any beforeEach, afterEach, and it declarations will all share the same reference to this, allowing you to avoid creating enclosed variables (like var controller, as seen above) and also avoid creating unnecessary globals. For example:
beforeEach(inject(function ($rootScope, $log, $controller) {
this.controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', function () {
this.value = controller.getValue();
// ... perform checks
});
Note the second argument in the call to $controller, this must be an object containing the expected dependencies that your controller ('MyController', in this case) relies upon.
The reasoning behind this is simply to allow developers to pass mock services, factories, providers, etc to the controller as an alternative to spies.
$controller: https://docs.angularjs.org/guide/unit-testing
this: http://jasmine.github.io/2.0/introduction.html
Apologies for the unuseful link to the Jasmine documentation regarding the usage of this with tests, I couldn't add a direct link to the correct section of the page due to how their anchor tags are set out (the anchor tag contains a <code></code> block, doh!).
Related
I'd like to create an utility class in Angular.js that can be used by several controllers.
So far I created this:
'use strict';
angular
.module('App')
.factory('AppUtils', AppUtils);
function AppUtils() {
var vm = this;
vm.getPersonOf = getPersonOf;
function getPersonOf(personId, allPersons) {
var person = {};
person.personId = {personId: personId};
allPersons.forEach(function(p) {
if (p.personId === personId) {
person = p;
}
});
return person;
}
}
And I tried to use it in a controller like this:
'use strict';
angular
.module('App')
.controller('AppController', AppController);
function AppController(personId, allPersons, AppUtils) {
var vm = this;
vm.personId = personId;
vm.person = AppUtils.getPersonOf(vm.personId, allPersons);
...
}
But I get this:
PhantomJS 1.9.8 (Windows 7 0.0.0) App should dismiss modal FAILED
Error: [$injector:undef] Provider 'AppUtils' must return a value from $get factory method.
http://errors.angularjs.org/1.5.0/$injector/undef?p0=InvoiceUnitsUtils
(The real names have been renamed to make it easier.)
Why am I getting that error? Am I not declaring properly the Utility module?
The factory is in charge of creating a service and handing its instance to you. To do this, you need to return your utility class:
function AppUtils() {
return {
getPersonOf: getPersonOf
// pass other utility functions...
}
function getPersonOf(personId, allPersons) {
var person = {};
person.personId = {personId: personId};
allPersons.forEach(function(p) {
if (p.personId === personId) {
person = p;
}
});
return person;
}
}
I removed the vm part because we are handing a service which usually has no view model (the controller is in charge of that, service is more of a business logic expert).
Here's some more information about the $get function in Angular's providers:
https://docs.angularjs.org/guide/providers
I have an angular application with some global environment variables defined in an env.js file:
(function(sp) {
'use strict';
pk.env = pk.env || {};
// localhost
pk.env.baseUrl = 'http://localhost:8080/';
})(typeof exports === 'undefined' ? (this.pk = this.pk || {}) : exports);
These variables are used in multiple factories to make REST API calls:
'use strict';
angular.module('pkApp').factory('pkFactory', PKFactory);
function PKFactory($http) {
var urlBase = pk.env.baseUrl;
var apiUrl = 'v1/data';
var _pkFactory = {};
_pkFactory.getData = function() {
return $http.get(urlBase + apiUrl);
};
return _pkFactory;
}
I am writing unit tests for this factory using Jasmine and I keep getting the error:
ReferenceError: Can't find variable: pk
If I remove this variable reference from the factory, the tests run fine.
'use strict';
console.log('=== In pk.factory.spec');
describe('Unit: pkFactory', function() {
beforeEach(module("pkApp"));
var $httpBackend, $rootScope, pkFactory;
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', 'v1/data').respond('Not found');
pkFactory = $injector.get('pkFactory');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('expects getData method to be defined', function(){
expect(pkFactory.getData()).toBeDefined();
$httpBackend.flush();
});
})
How do I inject value of 'pk.env.baseUrl' into the factory? I have tried using $window, but it didn't work.
As pretty much already answered here, you can also declare a global variable within your test file
var globalVar = "something";
describe('Your test suit', function() {
...
});
and if you are using Karma you can edit the karma.conf.js file to define it
// list of files / patterns to load in the browser
files: [
...,
'file-containing-the-global-variable.js'
],
You should avoid using the globals in Angular completely.
Convert the file to an angular value or constant:
angular.module('pkApp').value('pk', pk);
now you can change pkFactory to get the pk object injected
function PKFactory($http, pk) {
// pk is no longer from global scope, but injected from angular as an argument
var urlBase = pk.env.baseUrl;
var apiUrl = 'v1/data';
var _pkFactory = {};
_pkFactory.getData = function() {
return $http.get(urlBase + apiUrl);
};
return _pkFactory;
}
and in tests, you can now mock the pk to a different value (or not do anything and use the one from code)
I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.
This is Rails 4.0 App with angular-rails gem and jasmine gem. I also use angularjs-rails-resource.
I have simple controller:
app.controller('new', function ($scope, Appointment, flash) {
$scope.appointment = new Appointment();
$scope.attachments = [];
$scope.appointment.agendas_attributes = [];
$scope.createAppointment = function(){
$scope.appointment.attachments = $scope.attachments;
$scope.appointment.create().then(function(data){
flash(data.message);
}, function(error){
flash('error', error.data.message);
$scope.appointment.$errors = error.data.errors;
});
};
And on the unit test in Jasmine i want to isolate dependencies with jasmine.createSpyObj:
describe("Appointment new controller",function() {
var $scope, controller, mockAppointment, mockFlash, newCtrl;
beforeEach(function() {
module("appointments");
inject(function(_$rootScope_, $controller) {
$scope = _$rootScope_.$new();
$controller("new", {
$scope: $scope,
Appointment: jasmine.createSpyObj("Appointment", ["create"])
});
});
});
});
but i get an error:
TypeError: object is not a function
on line:
Appointment: jasmine.createSpyObj("Appointment", ["create"])
Can anybody help ? :-)
I believe the error is being thrown on this line:
$scope.appointment = new Appointment();
Appointment is an object literal, and not a function, so essentially you're trying to do this:
var x = {create: function(){}};
var y = new x();
But what you seem to want to do is this:
var x = function(){return {create: function(){}}};
var y = new x();
So make your mock like this:
Appointment: function() {
return jasmine.createSpyObj("Appointment", ["create"])
}
I see someone beat me to the answer;)
I have one suggestion that will make your describe block look cleaner
The module and inject statements can be nested inside the beforeEach definition:
describe("Appointment new controller", function () {
var $scope, controller, mockAppointment, mockFlash, newCtrl;
beforeEach(module("appointments"));
beforeEach(inject(function (_$rootScope_, $controller) {
$scope = _$rootScope_.$new();
$controller("new", {
$scope: $scope,
Appointment: function () {
return jasmine.createSpyObj("Appointment", ["create"])
}
});
}));
}
I am trying to include a library of functions, held in a factory, into a controller.
Similar to questions like this:
Creating common controller functions
My main controller looks like this:
recipeApp.controller('recipeController', function ($scope, groceryInterface, ...){
$scope.groceryList = [];
// ...etc...
/* trying to retrieve the functions here */
$scope.groceryFunc = groceryInterface; // would call ng-click="groceryFunc.addToList()" in main view
/* Also tried this:
$scope.addToList = groceryInterface.addToList();
$scope.clearList = groceryInterface.clearList();
$scope.add = groceryInterface.add();
$scope.addUp = groceryInterface.addUp(); */
}
Then, in another .js file, I have created the factory groceryInterface. I've injected this factory into the controller above.
Factory
recipeApp.factory('groceryInterface', function(){
var factory = {};
factory.addToList = function(recipe){
$scope.groceryList.push(recipe);
... etc....
}
factory.clearList = function() {
var last = $scope.prevIngredients.pop();
.... etc...
}
factory.add = function() {
$scope.ingredientsList[0].amount = $scope.ingredientsList[0].amount + 5;
}
factory.addUp = function(){
etc...
}
return factory;
});
But in my console I keep getting ReferenceError: $scope is not defined
at Object.factory.addToList, etc. Obviously I'm guessing this has to do with the fact that I'm using $scope in my functions within the factory. How do I resolve this? I notice that in many other examples I've looked at, nobody ever uses $scope within their external factory functions. I've tried injecting $scope as a parameter in my factory, but that plain out did not work. (e.g. recipeApp.factory('groceryInterface', function(){ )
Any help is truly appreciated!
Your factory can't access your $scope, since it's not in the same scope.
Try this instead:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.addToList = groceryInterface.addToList;
$scope.clearList = groceryInterface.clearList;
$scope.add = groceryInterface.add;
$scope.addUp = groceryInterface.addUp;
}
recipeApp.factory('groceryInterface', function () {
var factory = {};
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
factory.clearList = function() {
var last = this.prevIngredients.pop();
}
});
Alternatively, you can try using a more object oriented approach:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.groceryFunc = new groceryInterface($scope);
}
recipeApp.factory('groceryInterface', function () {
function Factory ($scope) {
this.$scope = $scope;
}
Factory.prototype.addToList = function (recipe) {
this.$scope.groceryList.push(recipe);
}
Factory.prototype.clearList = function() {
var last = this.$scope.prevIngredients.pop();
}
return Factory;
});
You cannot use $scope in a factory as it is not defined. Instead, in your factory functions change the properties of the object the factory is returning, e.g.
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
these will then get passed on to your $scope variable
$scope.addToList = groceryInterface.addToList;
// ... = groceryInterface.addToList(); would assign to `$scope.addToList` what is returned, instead of the function itself.
This isn't the exact answer for this question, but I had a similar issues that I solved by simply passing $scope as an argument to a function in my factory. So it won't be the normal $scope, but $scope at the time the function in the factory is called.
app.controller('AppController', function($scope, AppService) {
$scope.getList = function(){
$scope.url = '/someurl'
// call to service to make rest api call to get data
AppService.getList($scope).then(function(res) {
// do some stuff
});
}
});
app.factory('AppService', function($http, $q){
var AppService = {
getList: function($scope){
return $http.get($scope.url).then(function(res){
return res;
});
},
}
return AppService;
});