Suppose I have a service that depends on a value in $rootScope, as with the following (trivial) service:
angular.module('myServices', [])
.factory('rootValGetterService', function($rootScope) {
return {
getVal: function () {
return $rootScope.specialValue;
}
};
});
If I want to unit test this by putting a value in $rootScope, what is the best way to go about it?
...
var $rootScope;
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
...
By using provide(), you can inject a new $rootScope:
describe('in rootValGetter', inject(function ($rootScope) {
var scope;
var testRootValGetter;
beforeEach(function () {
scope = $rootScope.$new();
module(function ($provide) {
$provide.value('$rootScope', scope);
});
inject(function ($injector) {
testRootValGetterService = $injector.get('rootValGetterService');
});
});
it('getVal returns the value from $rootScope', function() {
var value = 12345;
scope.specialValue = value;
expect(testRootValGetterService.getVal()).toBe(value);
}
}
Include angular-mocks.js, then use angular.mock.inject:
Instead of creating a new scope as you would if you were injecting $scope you can mock the properties you need directly into $rootScope.
Then $rootScope will be injected with those properties available in the code you are testing.
At least this is the way I solved the same problem.
The following code should work in your example.
beforeEach(inject(function($rootScope) {
$rootScope.specialValue = 'whatever';
}));
Just try to give a more detailed answer including the test case:
...
var $rootScope;
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
...
it('getVal returns the value from $rootScope', function() {
var value = 12345;
$rootScope.specialValue = value;
expect(testRootValGetterService.getVal()).toBe(value);
}
Here's what I did:
it('some kind of wacky test', function($rootScope, Translate){
$rootScope.lang = 'en';
expect(Translate('overview').toBe('Overview');
}
Hope this helps others as this was the solution to a similar problem.
var rootValGetterService;
beforeEach(inject(function($rootScope,$injector) {
$rootScope.specialValue = "test";
rootValGetterService= $injector.get('rootValGetterService');
}));
it("Should have a service", function () {
expect(rootValGetterService).toBeDefined();
});
Related
My controller doesnt do a lot other than call methods in a service, the service wraps up and returns its functions, I have already written unit tests for the service mocking the http request.
Is it even worth unit testing the controller in this instance and if so what would I be testing as I have already tested the service functionality.
Below is my controller:
'use strict';
/* Controllers */
var calculatorControllers = angular.module('calculatorControllers', []);
calculatorControllers.controller('BodyController', ['$scope',
function($scope) {
$scope.toggleNavBarActive = function($event) {
$($event.currentTarget).parent().find('.active').removeClass('active');
$($event.currentTarget).addClass('active');
};
}]);
calculatorControllers.controller('CalculatorCtrl', ['$scope', 'CalculatorService',
function($scope, CalculatorService) {
$scope.orderProp = 'asc';
$scope.result = ' awaiting calculation';
$scope.sum = {};
$scope.add = function(val1, val2) {
var promise = CalculatorService.add(val1, val2);
promise.then(function(response) {
$scope.result = response;
});
};
}]);
calculatorControllers.controller('AboutCtrl', ['$scope', '$routeParams',
function($scope, $routeParams) {
}]);
Is it even worth unit testing the controller in this instance
Yes, you should aim for 100% coverage, not matter controller or service. I would test two things here (Jasmine):
it('inits $scope', function() {
var $scope = {};
$controller('PasswordController', { $scope: $scope });
expect($scope.orderProp).toEqual('asc');
expect($scope.result).toEqual(' awaiting calculation');
expect($scope.sum).toEqual({});
});
it('calls CalculatorService and sets the result', function() {
var $scope = {};
$controller('PasswordController', { $scope: $scope });
$scope.sum(1, 2);
expect(CalculatorServiceMock).toHaveBeenCalledWith(1, 2);
resolveCalculatorServiceMockAddSpyWith(3);
expect($scope.result).toEqual(3);
});
The only case when the controller methods don't require testing is
$scope.calculator = CalculatorService;
So all view calls like {{ calculator.sum(...) }} are done by the service.
In every other case controller methods should be tested. Since CalculatorService unit was already tested, it has to be mocked in order for controller logic to be tested in isolation.
Whenever, I am testing a controller and have something like this in it.
$scope.isSomething = function (Item) {
return ItemCollection.someItem(Item.attachedItem);
};
giving error on karma console:
TypeError: undefined is not an object (evaluating 'Item.attachedItem')
I am simply calling the function from the test file like this:
scope.isSomething();
I need to mock the Item.attachedItem or I am missing something here.. Please Explain in details as this is happening in multiple files.. thanks in advance
Also, for this type of code
.controller('itemCtrl', function (itemCollection) {
var vm = this;
this.itemCollection= itemCollection;
itemCollection.someItem().then(function (Item) {
vm.pageUrl = Item.pageUrl;
vm.Item= Item.someItems;
});
});
Also, this is also part of the code for more broad view here it gives Item.pageUrl is not a object error
Refer angular unit testing docs
The ItemCollection being a service, you could inject a mock while initialising a controller using
var ItemCollection, ItemCrtl;
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCrtl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
For Item, the method isSomething should take care of checking if Item is undefined before doing Item.attachedItem
Testing an aync block is tricky. someItem returns a promise. $q an angular service to which can be used create async functions while testing.
We need to resolve the deferred object to test the async task.
var ItemCollection, ItemCrtl, deferedObj;
beforeEach(inject(function($controller, $rootScope, $q) {
$scope = $rootScope.$new();
deferedObj = $q.defer();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCollection.someItem.andReturn(deferedObj.promise);
ItemCtrl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
it('sets page url', function() {
deferedObj.resolve({ pageUrl: 'http://url', someItems: [1,2,3] });
scope.$apply();
expect(ItemCtrl.pageUrl).toEqual('http://url');
});
you have to use mock Item data in test like this (assuming attachedItem value is boolean)
var item={attachedItem:true}
scope.isSomething(item)
$scope.isSomething = function (Item) {
if(!Item.attachedItem){
Item.attachedItem=YOUR_MOCK_VALUE;
}
return ItemCollection.someItem(Item.attachedItem);
};
So. I have simple controller and service:
angular
.module('field', [])
.controller('FieldController', function(FieldService) {
var vm = this;
vm.name = FieldService.getName();
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
})
;
Then i have some $interval in anotherService, that getting data every 1 second and calling FieldService.setName:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
});
})
But it won't change my HTML.
If i switch from primitive to object in returning value getName, then it's working.
Is there another approach? I personally think, that this structure i created is bad, but can't understand how it should be done.
JavaScript is always pass-by-value, but when your variable is an object, the 'value' is actually a reference to the object. So in your case, you are getting a reference to the object, not the value. So when the object changes, that change isn't propagated like a primitive would be.
Your code seems a bit incorrect, too. You are setting the value of response.name to FieldService.setName, which is actually a function.
If you want to use the getter/setter approach you have listed, then you could use events to let the controller know that name has changed.
.service('UpdateService', function($http, FieldService, $rootScope) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
$rootScope.$broadcast('nameChanged', {
name : response.name
});
});
});
})
.controller('FieldController', function(FieldService, $scope) {
var vm = this;
vm.name = FieldService.getName();
$scope.$on('nameChanged', function (evt, params) {
vm.name = params.name;
});
})
Another way to accomplish this is to use a $scope.$watch on the service variable in the controller:
.controller('FieldController', function($scope, FieldService) {
$scope.name = FieldService.getName();
$scope.$watch(function () {
return FieldService.getName();
}, function (newVal, oldVal) {
if (newVal !== oldVal) {
$scope.name = newVal;
}
});
})
I would move my $interval function inside a controller and then just update a $scope attribute every second. Then Angular will take care of the rendering.. Or you must also use an $interval function in your controller which gets the service content (ie FieldService.getName) every second.
I would use it this way:
angular
.module('field', [])
.controller('FieldController', function($scope, FieldService) {
$scope.name = function(){
FieldService.getName();
};
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
});
Use name() in your html to see the update value.And your other service:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
}, 1000);
})
There are numerous ways in which you can achieve this. No way is the best way. Depends on person to person.
Hope this helps.
There are several ways to solve that problem.
1) Move the $interval to controller.Then you will have a variable, which holds that data and you can bind it in view
2) You can use AngularJs Events.$rootScope will help you to send signal and catch it wherever you want.
If you want more info about this solutions, you can see it here:
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html
Trying to get this factory recognised in my other controllers so i can inject a result object into them.
myApp.factory('resultService', function(){
function SampleService() {
this.result = [];
}
});
This the the code in my controller with some removed that isn't purpose to the question.
myApp.controller('125Zero', ['$scope','ngAudio', function($scope, ngAudio, SampleService){
$scope.buttonPressed= function() {
var tempObj = {};
tempObj.title = $scope.title;
tempObj.frequency = $scope.frequency;
console.log(tempObj);
SampleService.result.push($scope.tempObj);
}
}]);
I keep receiving TypeError: Cannot read property 'result' of undefined.
i understand its probably something silly i've missed.
myApp.controller('125Zero', ['$scope','ngAudio', function($scope, ngAudio, SampleService){
You've nt injected SampleService in the array notation of dependencies.
myApp.controller('125Zero', ['$scope','ngAudio', 'resultService', function($scope, ngAudio, resultService){
Also you need to return an object from the factory. Currently you're not returning anything.
you need to do somethin like this:
myApp.factory('SampleService', function() {
return {
result: []
}
});
Maybe you are confused
This is your service
myApp.factory('resultService', function(){
this.result = [];
return this;
});
And you can use it in this way
myApp.controller('125Zero', ['$scope','ngAudio', 'resultService', function($scope, ngAudio, SampleService, resultService){
$scope.buttonPressed= function() {
var tempObj = {};
tempObj.title = $scope.title;
tempObj.frequency = $scope.frequency;
console.log(tempObj);
resultService.result.push($scope.tempObj);
}
}]);
I would like to know how to test some Angular scope variables at my controller that was created inside an ajax request.
What I mean is... This is my controller:
app.controller('NewQuestionCtrl', function ($scope, Question) {
loadJsonAndSetScopeVariables($scope, Question);
});
function loadJsonAndSetScopeVariables(scope, Question) {
Question.loadJson().then(function(success) {
var result = success.data.variables;
scope.levels = result.levels;
scope.tags = result.tags;
scope.difficulties = result.difficulties;
scope.questionTypes = result.questionTypes;
scope.areas = result.areas;
},function(data){
});
}
One of the prerequisites is not to use mock.
At my test I was able to inject my Question service:
describe('Controller: NewQuestionCtrl', function () {
beforeEach(angular.mock.module('testmeApp'));
var NewQuestionCtrl, scope, QuestionService;
beforeEach(inject(function ($controller, $rootScope, Question) {
scope = $rootScope.$new();
QuestionService = Question;
NewQuestionCtrl = $controller('NewQuestionCtrl', {
$scope: scope
});
}));
it('should attach a list of areas to the scope', function (done) {
expect(scope.areas).toBeDefined();
done();
});
Please, someone could help me?
Create a mock for Question and use that. There are several ways to do this. This is just one of them.
You could alternatively inject a real instance of Question and spy on that instead, but a mock is preferred to isolate these unit tests from the Question unit tests.
var questionDeferred, myController, scope;
var mockQuestion = {
loadJson: angular.noop
};
beforeEach(inject(function($q, $rootScope, $controller) {
questionDeferred = $q.defer();
scope = $rootScope.$new();
spyOn(mockQuestion, 'loadJson').and.returnValue(questionDeferred);
// Because your function is run straight away, you'll need to create
// your controller in this way in order to spy on Question.loadJson()
myController = $controller('NewQuestionCtrl', {
$scope: scope,
Question: mockQuestion
});
}));
it('should attach a list of areas to the scope', function (done) {
questionDeferred.resolve({/*some data*/});
scope.$digest();
expect(scope.areas).toBeDefined();
done();
});