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();
});
Related
I'm using ruby on rails with angularjs one, and testing it with teaspoon-jasmine for the first time and am running into issues. Basically, I have a controller that creates an empty array and upon load calls a factory method to populate that array. The Factory makes an http request and returns the data. Right now, i'm trying to test the controller, and i'm trying to test that 1) the factory method is called upon loading the controller, and 2) that the controller correctly assigns the returned data through it's callback. For a while I was having trouble getting a mocked factory to pass a test, but once I did, I realized I wasn't actually testing my controller anymore, but the code below passes. Any tips on how I can still get it to pass with mock, promises/callbacks, but accurately test my controller functionality. Or should I even test the this at all in my controller since it calls a factory method and just gives it a callback? My 3 files are below. Can anyone help here? It would be greatly appreciated
mainController.js
'use strict';
myApp.controller('mainController', [ 'mainFactory', '$scope', '$resource', function(factory, scope, resource){
//hits the /games server route upon page load via the factory to grab the list of video games
scope.games = [];
factory.populateTable(function(data){
scope.games = data;
});
}]);
mainFactory.js
'use strict';
myApp.factory('mainFactory', ['$http', '$routeParams', '$location', function(http, routeParams, location) {
var factory = {};
factory.populateTable = function(callback) {
http.get('/games')
.then(function(response){
callback(response.data);
})
};
return factory;
}]);
And finally my mainController_spec.js file
'use strict';
describe("mainController", function() {
var scope,
ctrl,
deferred,
mainFactoryMock;
var gamesArray = [
{name: 'Mario World', manufacturer: 'Nintendo'},
{name: 'Sonic', manufacturer: 'Sega'}
];
var ngInject = angular.mock.inject;
var ngModule = angular.mock.module;
var setupController = function() {
ngInject( function($rootScope, $controller, $q) {
deferred = $q.defer();
deferred.resolve(gamesArray);
mainFactoryMock = {
populateTable: function() {}
};
spyOn(mainFactoryMock, 'populateTable').and.returnValue(deferred.promise);
scope = $rootScope.$new();
ctrl = $controller('mainController', {
mainFactory: mainFactoryMock,
$scope: scope
});
})
}
beforeEach(ngModule("angularApp"));
beforeEach(function(){
setupController();
});
it('should start with an empty games array and populate the array upon load via a factory method', function(){
expect(scope.games).toEqual([])
mainFactoryMock.populateTable();
expect(mainFactoryMock.populateTable).toHaveBeenCalled();
mainFactoryMock.populateTable().then(function(d) {
scope.games = d;
});
scope.$apply(); // resolve promise
expect(scope.games).toEqual(gamesArray)
})
});
Your code looks "non-standard" e.g still using scope.
If you are just starting with angular I hardly recommend you to read and follow this:
https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
Angular controllers cannot be tested, extract the logic into factories/services and test from there.
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);
};
For some reason, even following the example provided by Josh in the post reply How to test John papa vm.model unit testing with jasmine?, I can't get my controller's values to show up in the testing area. I think it's because of the data service, but it is a necessary component for our SPA, as is using John Papa's styling.
Below is a code snippet to hold the code and display the errors I'm receiving.
(function() {
'use strict';
angular.module('tickets')
.service("DataService", DataService)
/* #ngInject */
DataService.$inject = ["$rootScope", "$q"];
function DataService($rootScope, $q) {
var vm = this;
vm.nameDefault = "Name -- Please Authenticate";
vm.name = vm.nameDefault;
};
})();
(function() {
'use strict';
angular.module('tickets')
.controller('HomeController', HomeController);
/* #ngInject */
HomeController.$inject = ['$scope', '$location', 'DataService'];
function HomeController($scope, $location, DataService) {
var vm = this;
vm.name = DataService.name;
vm.nameDefault = DataService.nameDefault;
};
})();
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular-mocks.js"></script>
<script>
'use strict';
describe('Controller: HomeController', function() {
beforeEach(module('tickets'));
var controller, $location, DataService;
var tests = 0;
beforeEach(inject(function($controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = {};
controller = $controller('HomeController', {});
}));
var controller, scope, $location, DataService;
var tests = 0;
/* // This version works up until I try to verify the name and nameDefault in controller \\
*
* beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
* $location = _$location_;
* DataService = _DataService_;
* scope = $rootScope.$new();
*
* controller = function () {
* return $controller('HomeController', {});
* };
* }));
*/
afterEach(function() {
tests += 1;
});
describe('local variables', function() {
describe('load the data model and values for all pertinent variables', function() {
it('should be instantiated', function() {
expect(DataService).toBeDefined();
});
it('should contain a name with an initial value before authentication', function() {
expect(DataService.nameDefault).toBe('Name -- Please Authenticate');
expect(DataService.name).toEqual(DataService.nameDefault);
});
});
describe('should load the controller with data model values, and update as data model values update', function() {
it('should be instantiated', function() {
expect(controller).toBeDefined();
})
it('should not have a vm attribute that can be reached from here', function() {
expect(controller.vm).toBeUndefined();
})
it('should contain a name with an initial value before authentication, both from the data model', function() {
expect(controller.name).toBe(DataService.name);
expect(controller.nameDefault).toBe(DataService.nameDefault);
});
});
});
it('should have tests', function() {
expect(tests).toBeGreaterThan(0);
});
});
</script>
My code, when I use it in our native environment, works to verify that everything in the data service has been instantiated properly (and using the commented out beforeEach block), but the styling using the example in the question referenced above throws even more errors about the scope not being instantiated, even though it is the same (with added dependencies) as that question.
I would expect the answer to be similar to the (currently unanswered) question: How to test John papa vm.model controllers and factories unit testing with jasmine?
I appreciate any help you guys offer.
-C§
Edit Even though I've answered and have success, I would love to get some feedback on why the implementation below works and the other two attempts do not. This is using Karma version 0.13.8 (latest), jasmine 2.1.0, and Angular 1.4.0.
I know it seems like I came up with the solution pretty quickly, but I've been wrestling with this since Friday afternoon (8/7) and have tried a dozen different formats without success.
Again, I welcome your comments and votes so that I can better understand why the version below works and the others have not, especially since I am still very green to AngularJS (1 month in, now).
Thanks again,
-C§
I get it now. Just had to look at Globally mock object in angularjs for jasmine/karma testing and my brain clicked.
The declaration and beforeEach block in the beginning of the test needs to look like this:
var controller, scope, $location, DataService;
var tests = 0;
beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = $rootScope.$new();
controller = $controller('HomeController', {
$scope: scope
});
}));
I think since I've messed with our initial setup a little too much (started the SPA from a template), I needed a strange implementation to make it all work. I'm now getting successful tests throughout:
I hope this helps someone else with similar issues.
So I have such decorator in app config:
angular.module('app').config(['$provide', function ($provide) {
$provide.decorator('$rootScope', ['$delegate', function ($delegate) {
$delegate.constructor.prototype.$onRootScope = function (name, listener) {
var unsubscribe = $delegate.$on(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchRootScope = function (name, listener) {
var unsubscribe = $delegate.$watch(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchAfterLoad = function (watchExpression, listener, objectEquality) {
var initialLoad = true;
this.$watch(watchExpression, function () {
if (initialLoad) {
// note: this obviously runs outside of angular, so sometimes the timeout could run after initial load
setTimeout(function () { initialLoad = false; }, 25);
} else {
listener.apply(this, arguments);
}
}, objectEquality);
};
return $delegate;
}]);
}]);
As you can see this decorator lets me use $scope.$onRootScope instead of $rootScope.$on and takes care of automatic listeners removal on scope destroy event...
When I unit test my code which logic contains $scope.$onRootScope I'm getting such error: TypeError: undefined is not a constructor (evaluating 'scope.$onRootScope') in
Before each test I'm loading all required models and do inject which looks like this ~:
beforeEach(function () {
inject(function (_$rootScope_) {
$rootScope = _$rootScope_;
});
});
How should I overcome this problem?
Is there a way to mock / mimic $scope.$onRootScope behaviour?
I'm quite new to unit testing & Jasmine so sorry for not very nicely formatted question.
EDIT #1:
As I'm mocking my $scope object (var $scope = {...}) before passing it as argument to service method which I'm testing I can avoid error by simply defining $scope method:
$scope = {
...
$onRootScope: function() {}
}
Still awaiting for some better ideas :-)
I believe you need to build your $scope based off of the decorated $rootScope, as opposed to creating a new dummy object.
Like so:
var $root, $scope;
beforeEach(function () {
module('app');
inject(function ($rootScope) {
$root = $rootScope;
$scope = $root.$new();
});
});
it('should have the expected property', function () {
expect($scope.constructor.prototype).to.have.property('$watchRootScope');
});
I'll chuck in a link to the spec suite of a mini-lib I put together some time ago, doing roughly the same thing you are now.
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();
});