A newbie Jasmine/Angular question.
I have a named function within a controller like so:
.controller( 'DummyCtrl', function DummyCtrl($scope){
var doSomething = function() {
return "blah";
};
})
I need to test this function, and am trying to by calling the following Jasmine spec:
describe ('myApp', function(){
var $scope, $controller;
var DummyCtrl;
beforeEach(module('myApp'));
describe('controllers', function(){
beforeEach(inject(function ($controller, $rootScope){
$scope = $rootScope.$new();
DummyCtrl = $controller('DummyCtrl', {$scope: $scope});
}));
describe( 'DummyCtrl', function(){
var blah;
beforeEach(function(){
blah = DummyCtrl.doSomething();
});
it('should do something', function(){
expect(blah).toContain("blah");
});
});
});
});
Instead of things working out, I result in the following error: TypeError: Object #<DummyCtrl> has no method 'doSomething'. I'm assuming this is something super simple that I'm not understanding.
The function DummyCtrl you are providing for the controller registration will be used by Angular as a constructor. If you need the controller instance to expose the function doSomething without attaching it to the $scope, you should attach it to this.
Try changing
var something = function(...
to
this.something = function(...
and your test should work.
You can see this approach here: http://jsfiddle.net/yianisn/8P9Mv/. Also have a look at this SO question: How to write testable controllers with private methods in AngularJs?
In a sense, using functions like that is private, and cannot be accessed from outside the function. Take a look at this link: http://javascript.crockford.com/private.html
Essentially what is said is that have a function/object in javascript, anything with a this. prefix is public, and anything with a var prefix is private.
For Angular, you can definitely have private variables and functions, if not just to lessen the memory usage of the $scope variable. Private functions should be called by your $scope objects to get values to be displayed/used by the user. Try changing it to this:
.controller( 'DummyCtrl', function DummyCtrl($scope){
var doSomething = function() {
return "blah";
};
$scope.something=doSomething();
})
And then testing the private function with:
describe( 'DummyCtrl', function(){
var scope = {},
ctrl = new DummyCtrl(scope);
it('should do something', function(){
expect(scope.something).toMatch('blah');
});
});
Related
I'm using the "controller as" syntax to create my controller. I have a private initialization function that calls a function to load the default data.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var mc = this;
mc.dataLoaded = false;
function init() {
mc.loadData();
}
mc.loadData = function(){
mc.dataLoaded = true;
}
init();
});
In my test I'm creating a spy to check whether the loadData function has been called. Although I can verify that the function has been called by testing for the mc.dataLoaded flag, my spy doesn't seem to record the function being called. How can I get the spy to correctly record the function call?
describe('Testing a Hello World controller', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
}));
it('should call load data', function() {
expect($scope.mc.loadData).toHaveBeenCalled();
//expect($scope.mc.dataLoaded).toEqual(false);
});
});
Plunker link
This sequence of lines:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
Means that the Jasmine spy is created after the controller has already been instantiated by $controller. Before the spy is created, the init function has already executed.
You can't switch the lines around either, because MainCtrl needs to exist before you can spy on a method on it.
If the init function calls another service, then spy on that service's method and assert that the service is called correctly. If MainCtrl is just doing something internally, then test the result of that, for example, by asserting that controller's data/properties are updated. It may not even be worth testing if it's trivial enough.
Also, since you're using the controller as syntax, you can reference the controller through the return value of calling $controller, rather than accessing the scope directly:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
ctrl.loadData === $scope.mc.loadData; // true
I found a solution that allowed me to avoid changing my controller. I included a $state mock service in the test suite's beforeEach method, and gave it a reload mock method:
beforeEach(inject(function ($controller, $rootScope) {
stateMock = {
reload: function() {
myCtrl = $controller('MyCtrl');
}
};
...
Then within the jasmine tests, I can simply call stateMock.reload() to re-initialize my controller while preserving my spies I declared in another beforeEach block.
I did this controller
app.controller('controller',['$scope','httpServices',function($scope,httpServices){
$scope.items= undefined;
httpServices.getItems( function(items){
$scope.items= items;
});
}]);
and I wrote this test
describe('controller', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('controller', {
'$scope': scope
});
}));
it('defined', function () {
expect(scope.items).toBeUndefined();
})
});
How I can test the scope.items after to have called the service?
I assume that your service httpServices is making some http requests. Therefore you should use the mock-backend service in order to test your controller.
Something like this, pay attention to the comments that I've made inside the code:
describe('Your specs', function() {
var $scope,
$controller,
$httpBackend;
// Load the services's module
beforeEach(module('yourApp'));
beforeEach(inject(function(_$controller_, $rootScope, _$httpBackend_) {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$controller = _$controller_;
//THIS LINE IS VERY IMPORTANT, HERE YOU HAVE TO MOCK THE RESPONSE FROM THE BACKEND
$httpBackend.when('GET', 'http://WHATEVER.COM/API/SOMETHING/').respond({});
var createController = function(){
$controller('controller', {$scope: $scope});
}
}));
describe('Your controller', function() {
it('items should be undefined', function() {
createController();
expect(scope.items).toBeUndefined();
});
it('items should exist after getting the response from the server', function () {
//THIS LINE IS ALSO VERY IMPORTANT, IT EMULATES THE RESPONSE FROM THE SERVER
$httpBackend.flush();
expect(scope.items).toBeDefined();
});
});
});
The question title states this is to test a service, but the code of the question looks like an attempt is being made to test the controller. This answer describes how to test the controller.
If you're testing the controller that calls httpServices.getItems, then you need to mock it/stub getItems in order to
Control it on the test
Not assume any behaviour of the real httpServices.getItems. After all, you're testing the controller, and not the service.
A way to do this is in a beforeEach block (called before the controller is created) provide a fake implementation of getItems that just saves the callback passed to it.
var callback;
beforeEach(inject(function(httpServices) {
callback = null;
spyOn(httpServices, 'getItems').and.callFake(function(_callback_) {
callback = _callback_;
});
});
In the test you can then call this callback, passing in some fake data, and test that this has been set properly on the scope.
it('saves the items passed to the callback on the scope', function () {
var testItems = {};
callback(testItems);
expect($scope.items).toBe(testItems);
});
This can be seen working at http://plnkr.co/edit/Z7N6pZjCS9ojs9PZFD04?p=preview
If you do want to test httpServices.getItems itself, then separate tests are the place for that. Assuming getItems calls $http, then you are most likely to need to use $httpBackend to handle mock responses. Most likely, these tests would not instantiate any controller, and I suspect not need to do anything on any scope.
I'm using angularJS and i understand how to test my $scope objects with karma-jasmine but i'm having difficulties testing regular functions and variables inside my controller file
//controller.js
angular.module('myApp').controller('mainCtrl', function ($scope) {
$scope.name = "bob";
var aNumber = 34;
function myFunction(string){
return string;
}
});
what i would like to do is to test to see if expect(aNumber).toBe(34);
// test.js
describe('Controller: mainCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
var mainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
mainCtrl = $controller('mainCtrl', {
$scope: scope
});
}));
// understand this
it('should expect scope.name to be bob', function(){
expect(scope.name).toBe('bob');
});
// having difficulties testing this
it('should expect aNumber to be 34', function(){
expect(aNumber).toBe(34);
});
// having difficulties testing this
it('should to return a string', function(){
var mystring = myFunction('this is a string');
expect(mystring).toBe('this is a string');
});
});
It looks you are trying to test private variables declared in angular controller. The variables which are not exposed through the $scope cannot be tested, as they are hidden, and are visible only in a function scope inside the controller. More on private members and information hiding in javascript you can find here
The way how you should approach private fields in tests is by testing them through exposed api. If the variable is not used in any exposed publicly method it means that it's not used so it doesn't make sense to keep it and test it.
In an angular controller I have a function on my scope that looks like this:
$scope.myFunction = function(val) {
$scope.myVar = val;
$scope.mySettings.myPath = $scope.myBase + $scope.myVar;
}
In my karma unit test I want to trigger that function on the controller using a value i supply ... then check $scope.mySettings.myPath to make sure it is what i think it should be.
Im trying this in karma:
describe("Unit Testing: Controller Testing" , function() {
describe('myController' , function () {
var scope,ctrl;
beforeEach(module('myApp.controllers'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('myController', {
$scope: scope
});
}));
it('should have a working changeVideo function' , function(){
scope.changeVideo(scope.myVar[6]);
expect(scope.mySettings.myPath).to.equal('Expected String');
});
});
});
The error i see is that it cannot call equal method of undefined. Which is odd because scope.mySettings.myPath is actually predefined in the controller so it should never be undefined.
To give some background I do have access to the controller and the scope from karma. This one is stumping me though.
The solution to this was using toEqual() instead of to.equal()
I get the following error: TypeError: undefined is not a function
The problem is that the common is module and a factory and the problem is on my line
var ctrl = $controllerConstructor("resetPasswordSentScreen", { $scope: scope, common: common});
Here is the full test:
describe('resetPasswordSentScreen', function () {
var scope, $controllerConstructor;
beforeEach(module('common', 'app'));
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controllerConstructor = $controller;
}));
it('it should navigate to the correct url when backToLogin is called ', function (common) {
var ctrl = $controllerConstructor("resetPasswordSentScreen", { $scope: scope, common: common });
var mocklocation = sinon.stub({ url: function () {}});
expect(scope.backToLogin()).toBe(mocklocation.url);
});
});
That is not the problem, the problem is that you can't inject stuff into your functions like you do in your code. To inject you need to call inject as you did in the beforeEach. So, if you want to inject that factory, you need this:
it("message", inject(function(common) {
...
}));
There is how you inject. That should work.