I am trying to set up Jasmine testing on my Angular Application to test a controller.
Controller:
var navigation = angular.module("navigation", []);
navigation.controller("NavigationController", ['$scope', function ($scope) {
$scope.myObject = [];
$scope.tabs = [
{ title: "Surcharge Index", content: "SurchargeIndex" },
{ title: "Scheduling and Adjustments", content: "Scheduling" },
{ title: "Auto Update Settings", content: "Automation" },
{ title: "Processing Rules", content: "FuelProcessing" },
{ title: "Data Update ", content: "DataUpdate" },
];
}]);
Test:
describe("NavigationController", function () {
var scope;
var controller;
//beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('NavigationController', { '$scope': scope });
}));
it("scope is defined", function () {
expect(scope).toBeDefined();
//expect(scope.tags[0].title).toBe('Doe Index');
});
it("should contain a list of tabs", function () {
//expect(scope).toBeDefined();
expect(scope.tags).toContain({ title: 'Doe Index' });
});
});
Neither Jasmine test is ever run.
Test page:
Jasmine2.0.0finished in 0.001s
raise exceptions
Ran 0 of 2 specs - run all
0 specs, 0 failures
NavigationController
scope is defined
should contain a list of tabs
This is what Jasmine returns. For some reason none of the tests are being run.
Any suggestions?
You have to load the module you are testing:
beforeEach(module('navigation'));
Add that where you have:
//beforeEach(module('app'));
But uncommented.
I used this OdeToCode post as a sample once I realized that I needed to load the module. My test case now looks like this.
describe("myApp", function () {
beforeEach(module('navigation'));
describe("NavigationController", function() {
var scope;
var controller;
beforeEach(inject(function($controller, $rootScope) {
jasmine.addMatchers(customMatcher);
scope = $rootScope.$new();
controller = $controller('NavigationController', { '$scope': scope });
}));
it("scope is defined", function() {
expect(scope).toBeDefined();
});
it("should contain a list of tabs", function() {
expect(scope.tabs.length).toBe(5);
});
});
)};
Related
I'm adding unit tests to my AngularJS application, and running into an issue where the controller I'm testing is aware of my mocks (logs the correct mock object), but my unit tests cannot return the mock value. I've been stuck on this one for a while.
Mock Service
angular.module('mocks.myService', [])
.factory('myService', function() {
var service = {
hi : 'hi'
};
return service;
});
Controller
.controller('MyCtrl', ['myService', function(myService) {
var vm = this;
vm.doThing = function() {
console.log(myService);
// Object{hi:'hi'}
};
}]);
Unit Test
describe('myApp.selection module', function() {
var myCtrl, myService;
beforeEach(function() {
myService = module('mocks.myService');
console.log(myService);
// undefined
});
describe('controller', function(){
it('should exist', inject(function($controller) {
myCtrl = $controller('MyCtrl');
expect(myCtrl).toBeDefined();
}));
it ('should do thing', function() {
myCtrl.doThing();
});
});
});
Try this:
describe('myApp.selection module', function () {
var myCtrl, myService;
beforeEach(module("mocks.myService"));
beforeEach(inject(function (_myService_) {
myService = _myService_;
console.log(myService);
}));
describe('controller', function () {
it('should exist', inject(function ($controller) {
myCtrl = $controller('MyCtrl');
expect(myCtrl).toBeDefined();
}));
it('should do thing', function () {
myCtrl.doThing();
});
});
});
source: https://docs.angularjs.org/guide/unit-testing
I have a simple controller in my angular app, and the jasmine test spec for the same returns a Reference Error.
My Controller code:
'use strict';
angular.module('taskListAppApp')
.controller('MainCtrl', function ($scope) {
$scope.todoList = [{
todoText: 'In case of Fire',
done: false
}, {
todoText: 'git commit',
done: false
}, {
todoText: 'git push',
done: false
}, {
todoText: 'exit the building!',
done: false
}];
$scope.getTotalTodos = function () {
return $scope.todoList.length;
};
$scope.todoAdd = function () {
// Checking for null or empty string
if (null !== $scope.taskDesc && "" !== $scope.taskDesc) {
$scope.todoList.push({
todoText: $scope.taskDesc,
done: false
});
}
};
// Function to remove the list items
$scope.remove = function () {
var oldList = $scope.todoList;
$scope.todoList = [];
angular.forEach(oldList, function (x) {
if (!x.done) {
$scope.todoList.push(x);
}
});
};
});
And my test spec:
"use strict"
describe('Controller: MainCtrl', function () { //describe your object type
// beforeEach(module('taskListNgApp2App')); //load module
beforeEach(angular.mock.module('taskListAppApp'));
describe('MainCtrl', function () { //describe your app name
var todoCtrl2;
beforeEach(inject(function ($controller, $rootScope) {
var scope = $rootScope.$new();
todoCtrl2 = $controller('MainCtrl', {
//What does this line do?
$scope: scope
});
}));
it('should have todoCtrl defined', function () {
expect(todoCtrl2).toBeDefined();
});
it('trial test for toEqual', function(){
var a = 4;
expect(a).toEqual(4);
});
//THESE 2 FAIL
it('should have todoList defined', function() {
expect(scope.todoList).toBeDefined();
});
it('should have add method defined', function(){
expect(todoCtrl2.todoAdd()).toBeDefined();
});
});
});
The error I get is:
PhantomJS 2.1.1 (Linux 0.0.0) Controller: MainCtrl MainCtrl should have add method defined FAILED
TypeError: undefined is not a function (evaluating 'todoCtrl2.todoAdd()') in test/spec/controllers/main.spec.js (line 58)
test/spec/controllers/main.spec.js:58:28
loaded#http://localhost:8080/context.js:151:17
PhantomJS 2.1.1 (Linux 0.0.0): Executed 4 of 4 (2 FAILED) (0.05 secs / 0.02 secs)
I tried other ways to call the objects/functions, but the last 2 tests are failing every time with the same error viz. ReferenceError
Where am I going in calling the objects?
You need to declare var scope outside the function. your scope variable is undefined in your test case.
Try this
describe('Controller: MainCtrl', function () { //describe your object type
var scope;
// beforeEach(module('taskListNgApp2App')); //load module
beforeEach(angular.mock.module('taskListAppApp'));
describe('MainCtrl', function () { //describe your app name
var todoCtrl2;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
todoCtrl2 = $controller('MainCtrl', {
//What does this line do?
$scope: scope
});
}));
it('should have todoCtrl defined', function () {
expect(todoCtrl2).toBeDefined();
});
it('trial test for toEqual', function(){
var a = 4;
expect(a).toEqual(4);
});
//THESE 2 FAIL
it('should have todoList defined', function() {
expect(scope.todoList).toBeDefined();
});
it('should have add method defined', function(){
expect(scope.todoAdd).toBeDefined();
});
});
});
I have a directive that consumes a service:
angular.module('app', [])
.service('myService', function() {
return {
getCustomer: function(){
return {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}
};
})
.directive('myCustomer', function(myService) {
return {
link: function(scope){
scope.customer = myService.getCustomer();
},
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
I'm trying to unit test this, but I can't seem to figure out how to inject the service into my directive in the unit test.
var tests;
(function (tests) {
describe('myCustomer Directive', function () {
var scope, createDirective;
beforeEach(angular.mock.module('app'));
beforeEach(angular.mock.module('templates'));
beforeEach(angular.mock.inject(function ($injector) {
var $compile = $injector.get('$compile');
var myService = $injector.get('myService');
scope = $injector.get('$rootScope');
// where do I inject myService???
createDirective = function () {
return $compile('<my-customer></my-customer>')(scope);
};
}));
describe('on creation', function () {
var sut;
beforeEach(function () {
sut = createDirective();
scope.$digest();
});
it('creates an element node', function () {
var contents = sut.contents();
expect(contents[0].nodeType).toBe(sut[0].ELEMENT_NODE);
});
});
});
})(tests || (tests = {}));
The problem is that I need to be able to explicitly inject the dependency so I can mock some of it's calls. Is this possible?
Here's a Plunker with my app code.
So turns out, as JB Nizet pointed out, that all you need to do is spy on the service and it all takes care of itself.
beforeEach(angular.mock.inject(function ($injector) {
var $compile = $injector.get('$compile');
scope = $injector.get('$rootScope');
var myService = $injector.get('myService');
// set up spies
spyOn(myService, "getCustomer").and.returnValue({foo: 'bar'});
createDirective = function () {
return $compile('<my-customer></my-customer>')(scope);
};
}));
I have a custom directive that uses an external template and is passed data from a service. I decided to ensure that the promise was resolved before modifying the data, which was fine in the actual code but broke my unit tests, which is annoying. I have tried a number of variations but am now stuck. I am using 'ng-html2js' preprocessor.
Here is the unit test
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
deferred = $q.defer();
promise = deferred.promise;
promise.then(function (things) {
scope.items = things;
});
// Simulate resolving of promise
deferred.resolve(things);
// Propagate promise resolution to 'then' functions using $apply().
scope.$apply();
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
I have left out the custom addMatchers and the rest of the tests. The error I get is
TypeError: 'undefined' is not an object (evaluating 'scope.items.$promise')
Here is the directive
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.items.$promise.then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
This is where the directive is used in main.html
<cc-accordion items="genres"></cc-accordion>
In the main controller the genres service is passed in ie
angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query();
}]);
Okay, I would move that code you put in link into the controller. The data processing should probably happen in a service. I know you've been told big controllers are bad, but big linking functions are generally worse, and should never do that kind of data processing.
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query().then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
});
Your link function is now empty. Define items on the rootScope instead, this ensures that the isolateScope and your directive interface are working correctly.
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
scope.items = things; // Tests your directive interface
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
The behavior of the promise should be tested in a controller test, by mocking the return value of the service. Your problem with the $promise test has been solved.
The actual issue was that you were assuming that $q.defer() gave you the same kind of promise as the angular $http, but that is solved by design instead.
As peter said remove the promise from the directive and add it to the controller
angular.module('magicApp')
.controller('MainCtrl', ['$scope', 'Genre',
function ($scope, Genre) {
$scope.genres = Genre.query();
$scope.genres.$promise.then(function () {
angular.forEach($scope.genres, function (genre) {
genre.selected = false;
});
$scope.genres[0].selected = true;
});
}]);
This will also allow the controller to specify which tab is selected to begin with.
In the directive
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
The directive unit test now looks like this
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
beforeEach(function () {
jasmine.addMatchers({
toHaveClass: function () {
return {
compare: function (actual, expected) {
var classTest = actual.hasClass(expected);
classTest ? classTest = true : classTest = false;
return {
pass: classTest,
message: 'Expected ' + angular.mock.dump(actual) + ' to have class ' + expected
};
}
};
}
});
});
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
scope.genres = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
it('should bind the content', function () {
var contents = elm.find('.cc-accord-content div:first-child');
expect(contents.length).toBe(2);
expect(contents.eq(0).text().trim()).toBe('Scifi description');
expect(contents.eq(1).text().trim()).toBe('Comedy description');
});
it('should change active content when header clicked', function () {
var titles = elm.find('.cc-accord h2'),
divs = elm.find('.cc-accord');
// click the second header
titles.eq(1).find('a').click();
// second div should be active
expect(divs.eq(0)).not.toHaveClass('active');
expect(divs.eq(1)).toHaveClass('active');
});
});
And the unit test for main controller now has the added property of selected
'use-strict';
describe('magicApp controllers', function () {
// using addMatcher because $resource is not $http and returns a promise
beforeEach(function () {
jasmine.addMatchers({
toEqualData: function () {
return {
compare: function (actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(module('magicApp'));
beforeEach(module('magicServices'));
describe('MainCtrl', function () {
var scope, ctrl, $httpBackend;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/api/genres').
respond([{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {$scope: scope});
}));
it('should create "genres" model with 2 genres fetched from xhr', function () {
expect(scope.genres).toEqualData([]);
$httpBackend.flush();
expect(scope.genres).toEqualData(
[{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
});
});
});
I want to do integration testing of my AngularJs application. I want to test the actual service not mocking it. Somehow grabbing the instance of my service from the test does not work. Code is below:
var todoApp = angular.module('todoApp', []);
todoApp.controller('TodoController', function ($scope, todoService) {
$scope.FinalMessage = 'Hello World!';
this.getTodos = function() {
$scope.Todos = todoService.getTodos();
};
});
// TODO: move it to its file
todoApp.service("todoService", function () {
// TODO: use the REST to grab the values...
this.getTodos = function () {
var todos = [
{ TodoId: 1, Description: "Todo 1", Completed: false },
{ TodoId: 1, Description: "Todo 2", Completed: true }
];
return todos;
};
});
The test is:
describe("Integration testing with the Todo service...", function () {
describe("Todo Controller test", function () {
beforeEach(module("todoApp")); // From angular mock not the real module!!
it("Tests the controller returns the message", (inject(function ($rootScope, $controller) {
var $injector = angular.injector(['todoApp']);
var myService = $injector.get('todoService');
//var service = module.service("todoService", todoService);
var scope = $rootScope.newValue();
var controller = $controller("TodoController", { $scope: scope, todoService: myService });
controller.getTodos();
expect(scope.Todos).not.toBe(null);
})));
});
});
somehow I cannot instantiate the todoService?
thanks
You are trying to instantiate the service using the injector. The TodoService is already part of the TodoApp module so ng already knows how to find it and will instantiate it for you.
Try rewriting your spec like this:
(see working example on plnkr => http://plnkr.co/edit/Q2f6SJ?p=preview)
describe("Integration testing with the Todo service...", function () {
describe("Todo Controller", function () {
var $rootScope;
beforeEach(module("todoApp"));
beforeEach(inject(function(_$rootScope_){
$rootScope = _$rootScope_;
}));
it("Should return the message", (inject(function ($rootScope, $controller, todoService) {
var scope = $rootScope.$new();
var controller = $controller("TodoController", { $scope: scope, todoService: todoService });
controller.getTodos();
expect(scope.Todos).not.toBe(null);
})));
});
});
To test my app with real backend calls I used a modified version of angular-mocks
It works just like for unit-tests in Jasmine.
I'm using it with Jasmine 2.0, so a test looks like following :
it(' myTest', function (done) {
_myService.apiCall()
.then(function () {
expect(true).toBeTruthy();
done()
});
});
NB: the done is needed because of the async call.