I am trying to unit test an angularjs controller using Node.js. I am using gulp.js and mocha to run the tests, via gulp-mocha.
This is what my gulpfile.js looks like right now:
(function () {
var gulp = require('gulp');
var mocha = require('gulp-mocha');
gulp.task('default', function () {
return gulp
.src('Scripts/Tests/*.js', { read: false })
.pipe(mocha({
ui: 'tdd',
reporter: 'dot',
globals: {
angular: require('./Scripts/angular.js')
}
}));
});
})();
This is the code under test:
(function () {
var application = angular.module('Main');
application.controller('MainController', ['$scope', function ($scope) {
$scope.isDisabled = function () {
return true;
};
}]);
})();
And this is the test file itself:
(function () {
var chai = require('chai');
var assert = chai.assert;
angular.module('Main', []); // Create an empty module
require('../MainController.js'); // Fill the module with the controller
suite('Main Controller', function () {
test('isDisabled always true', function () {
var controllerFactory = angular.injector.get('$controller');
var controller = controllerFactory('MainController', {
'$scope': {}
});
var result = controller.isDisabled();
assert.isTrue(result);
});
});
})();
I need to make angular a global in order for my test and the file I am testing to work. However, the call to require in gulpfile.js is giving me a Reference Error: window is not defined error. That makes sense, since I am running in Node.js.
What do I need to do to load up angular in Node.js?
As #unobf said (and the angularjs documentation says), the trick to testing angular from Node.js was to use Karma. That meant installing karma and karma-mocha, along with karma-chrome-launcher, karma-ie-launcher, karma-firefox-launcher (or whatever) via npm install.
Then I basically had to redo my gulp.js file from scratch:
(function () {
var gulp = require('gulp');
var karma = require('karma').server;
gulp.task('runTests', function (done) {
karma.start({
configFile: __dirname + '/karma.config.js',
singleRun: true
}, done);
});
gulp.task('default', ['runTests']);
})();
I also had to create a karma.config.js file to configure karma to use Chrome, Firefox, IE with mocha. In the same file, I configured mocha to use the tdd UI and the dot reporter:
module.exports = function(config) {
config.set({
browsers: ['Chrome', 'Firefox', 'IE'],
frameworks: ['mocha'],
files: [
'./Scripts/angular.js',
'./Scripts/chai.js',
'./Scripts/*.js',
'./Scripts/Tests/*.js'
],
singleRun: true,
client: {
mocha: {
reporter: 'dot',
ui: 'tdd'
}
}
});
};
I learned you have to specify angularjs, chai.js, etc. first so they are picked up first and in global scope for the other files. Along the same lines, the files for the code being tested have to be listed before the test files.
The code being tested didn't change at all. Once my tests were running, I realized my test was broken, and it ended up looking like this to pass:
(function () {
var assert = chai.assert;
suite('Main Controller', function () {
test('isDisabled always true', function () {
var injector = angular.injector(['ng', 'Main']);
var $controller = injector.get('$controller');
var scope = {};
var controller = $controller('MainController', {
'$scope': scope
});
var result = scope.isDisabled();
assert.isTrue(result);
});
});
})();
The big take away was that the controller simply populates the $scope object. I call isDisabled() on the $scope object instead. Of course, getting at the controller is a lot of work, so it makes more sense to use the inject method provided by angular-mocks.js:
(function () {
var assert = chai.assert;
suite('Main Controller', function () {
var $scope = {};
setup(function () {
module('Main');
inject(function ($controller) {
$controller('MainController', {
'$scope': $scope
});
});
});
test('isDisabled always true', inject(function ($controller) {
var result = $scope.isDisabled();
assert.isTrue(result);
}));
});
})();
Hopefully this is enough to get anyone started who is trying to use Node.js and mocha to test angularjs code.
I am doing this in Grunt, but the same principle applies to Gulp. You need to inject the "angular-mocks.js" file to be able to mock the dependency injection. I am using Karma http://karma-runner.github.io/0.12/index.html to do this and set it up in my karma.conf.js file as follows:
module.exports = function(config){
config.set({
basePath : '../../',
files : [
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
...
Then you can inject a $controller into your tests and do things like test your controller initialization. The test below is initializing the scope and then testing that the controller is adding methods to the scope (note, I am using Jasmine, but the same can be done with mocha) but Jasmine comes with some nice builtin spy capabilities.
describe('analysisController', function () {
var scope, state;
beforeEach(function () {
module('analysis');
scope = {
$apply:jasmine.createSpy(),
ws: {
registerCallback:jasmine.createSpy(),
sendMessage:jasmine.createSpy()
}
},
state = {
go : jasmine.createSpy()
};
});
it('should add a bunch of methods to the scope', inject(function ($controller) {
$controller('analysisController', {
$scope : scope,
$state: state
});
expect(typeof scope.colorContrast).toBe('function');
expect(typeof scope.XPathFromIssue).toBe('function');
}));
...
Related
I can't get these two spec files to play well with each other. I didn't think spec files would effect other spec files but in this case it seem like they do, it makes no sense to me.
I'm using Jasmine and Karma the tests are automated with Gulp
The error I'm getting is "Unknown provider: ProductServiceProvider <- ProductService"
I have changed the tests to troubleshoot the issue here is the simple versions.
If I comment out the following line in file 2 both files pass.
angular.module('eu.product.service', []);
It has something to do with mocking the module but I can't figure out what I'm doing wrong here.
spec file 1
describe('Testing euProduct', function(){
var $factory;
var $httpBackend;
beforeEach(function () {
//modules
module('eu.product.service');
//injections
inject(function($injector){
$factory = $injector.get('ProductService');
$httpBackend = $injector.get('$httpBackend');
});
//mock data
$httpBackend.when('GET', '/Mercury/product/list/0/0?PrimaryCategoryID=0&pageSize=20&startPage=1').respond({
"data":
[{
"recallid":45,
}]
});
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
//-----Tests----
it('Should be able to get data from the server on default parameters.', function(){
$factory.list({},function(data){
expect(data.data[0].recallid).toBe(45);
});
$httpBackend.flush();
});
});
Spec file 2
'use strict';
describe('Testing euProduct Logic', function(){
//variables in closure scope so they can be used in tested but set with injection in beforeEach
var $factory;
//mocking a module :: http://www.sitepoint.com/mocking-dependencies-angularjs-tests/
beforeEach(function () {
angular.module('eu.product.service',[]);
module(function($provide) {
$provide.factory('ProductService', function() {
// Mocking utilSvc
return {
list : function(para, callback){
callback({
data : {
product : 'The product Name'
}
})
}
};
});
$provide.service('storageSvc', function() {
// Mocking storageSvc
});
});
//modules
module('eu.product.logic');
//injections
inject(function($injector){
$factory = $injector.get('ProductLogic');
});
});
//-----Tests----
it('Should be able to run tests', function(){
expect(2).toBe(2);
});
});
Both module and inject from angular-mocks return functions which need to be called.
In the following example I made these changes:
Refactor to a basic working example
Don't define custom $-prefixed variables. These are reserved by angular.
Use inject to inject instead of $injector.
Add some comments for further explanation.
describe('ProductService', function() {
var ProductService;
var $httpBackend;
// Start module config phase.
beforeEach(module('eu.produce.service', function($provide) {
// Inject providers / override constants here.
// If this function is empty, it may be left out.
}))
// Kickstart the app and inject services.
beforeEach(inject(function(_ProductService_, _$httpBackend_){
ProductService = _ProductService_;
$httpBackend = _$httpBackend_;
});
beforeEach(function() {
// Optionally use another beforeEach block to setup services, register spies, etc.
// This can be moved inside of the inject function as well if you prefer.
//mock data
$httpBackend.when('GET', '/Mercury/product/list/0/0?PrimaryCategoryID=0&pageSize=20&startPage=1').respond({
"data":
[{
"recallid":45,
}]
});
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
//-----Tests----
it('Should be able to get data from the server on default parameters.', function(){
ProductService.list({},function(data){
expect(data.data[0].recallid).toBe(45);
});
$httpBackend.flush();
});
});
I'm facing some trouble with a generated project (by yoeman) and it's testing.
What I want is to build a fully automated testing environement. I use Gulp, Karma, Jasmine and angular-mocks.
TypeError: undefined is not an object (evaluating 'controller.awesomeThings')
In my console after 'gulp test' it throws this error message. but this doesn't make any sense for me.
Look at my test spec:
'use strict';
describe('Controller: AboutCtrl', function() {
// load the controller's module
beforeEach(function() {
module('evoriApp');
});
var controller;
var scope;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('AboutCtrl', {
'$scope': scope
});
}));
it('should attach a list of awesomeThings to the scope', function() {
console.log(controller);
expect(controller.awesomeThings.length).toBe(3);
});
});
And karma should have any file it needs (karma.conf.js):
files: [
// bower:js
'bower_components/angular/angular.js',
'bower_components/angular-animate/angular-animate.js',
'bower_components/angular-aria/angular-aria.js',
'bower_components/angular-cookies/angular-cookies.js',
'bower_components/angular-messages/angular-messages.js',
'bower_components/angular-resource/angular-resource.js',
'bower_components/angular-route/angular-route.js',
'bower_components/angular-sanitize/angular-sanitize.js',
'bower_components/angular-material/angular-material.js',
// endbower
'app/scripts/**/*.js',
'test/mock/**/*.js',
'test/spec/**/*.js'
],
This is how the generated gulp method looks like:
gulp.task('test', ['start:server:test'], function () {
var testToFiles = paths.testRequire.concat(paths.scripts, paths.mocks, paths.test);
return gulp.src(testToFiles)
.pipe($.karma({
configFile: paths.karma,
action: 'watch'
}));
});
I don't know where the failure is. I checked all paths and rewrote the testfiles multiple times... But the error doesn't change.
Does anybody have an idea, what the error could be caused by?
Ok guys, I got it.
I tried to run the test in the jasmine standalone environement to minimies the area where the error root is.
This is what my controller looks like:
angular.module('evoriApp')
.controller('AboutCtrl', function ($scope) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
$scope.testVariable = 2;
});
I rewrote the 'this.awesomeThings' to '$scope.awesomeThings' and didn't get it, that this is something different. Whatever...
This is the now running spec:
describe('Controller: AboutCtrl', function() {
// load the controller's module
beforeEach(function() {
module('evoriApp');
console.log("1");
});
var controller;
var scope;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('AboutCtrl', {
$scope: scope
});
}));
it('sollte instanziert worden sein', function() {
expect(controller).toBeDefined();
});
it('sollte ein Object "awesomeThings" besitzen', function() {
expect(scope.awesomeThings).toBeDefined();
});
it('should attach a list of awesomeThings to the scope', function() {
expect(scope.awesomeThings.length).toBe(3);
});
});
What I learned: In a test you have to generate the scope, which you pass to the controller, by yourself and afterward you got to use the passed scope variable for $scope.variables.
I'm having trouble getting my tests to run due to dependencies not beeing injected correctly.
The error I'm getting is defined in the title. I've included the actual test code, the app.js & index.html file from my solution.
The problem lies with the deferred bootstrap which I'm not fimiliar with as it was included by one of my colleagues. If I remove the "app.config(function (STARTUP_CONFIG..." from the app.js file then the test runs fine
How can I correctly inject the STARTUP_CONFIG in my test?
test code
..
..
describe("test description...", function () {
var app;
var mockupDataFactory;
beforeEach(module('Konstrukt'));
beforeEach(inject(function (STARTUP_CONFIG,BUDGETS,APPLICATIONS) {
//failed attempt to inject STARTUP_CONFIG
}));
beforeEach(function () {
app = angular.module("Konstrukt");
});
beforeEach(function () {
mockupDataFactory = konstruktMockupData.getInstance();
});
it('should be accessible in app module', function () {
expect(app.pivotTableService).toNotBe(null); //this test runs fine
});
it('test decr...', inject(function ( pivotTableService) {
... //Fails here
..
..
app.js
..
..
angular.module('Konstrukt', ['ngGrid', 'ngSanitize', 'ngRoute','pasvaz.bindonce', 'ngAnimate', 'nvd3ChartDirectives', 'ui.select', 'ngProgress', 'ui.grid', 'ui.grid.edit','ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.pinning', 'ui.grid.resizeColumns']);
var app = angular.module('Konstrukt');
app.config(function (STARTUP_CONFIG, BUDGETS, APPLICATIONS) {
var STARTUP_CONFIG = STARTUP_CONFIG;
var BUDGETS = BUDGETS;
var APPLICATIONS = APPLICATIONS;
});
..
..
index.html
..
..
<script>
setTimeout(function(){
window.deferredBootstrapper.bootstrap({
element: window.document.body,
module: 'Konstrukt',
resolve: {
STARTUP_CONFIG: ['$http', function ($http) {
return $http.get('/scripts/_JSON/activeBudgets.JSON');
}],
BUDGETS: ['$http', function ($http) {
return $http.get('/scripts/_JSON/activeBudgets.JSON');
}],
APPLICATIONS: ['$http', function ($http) {
return $http.get('/scripts/_JSON/applications.JSON');
}]
}
})
} , 1500);
</script>
The deferredBootstrapper will not run in your unit tests, which means the constants it normally adds to your module won't be available.
You can add a global beforeEach that provides mocked versions of them:
beforeEach(function () {
module(function ($provide) {
$provide.constant('STARTUP_CONFIG', { something: 'something' });
$provide.constant('BUDGETS', { something: 'something' });
$provide.constant('APPLICATIONS', { something: 'something' });
});
});
I am trying to test an angularjs service called MyService. If I try to inject it, seems that angular tries to use it before is loaded. On the other hand, if I mock MyService via $provide and so on, it works but I will not have the actual object to test.
(function (angular) {
'use strict';
angular.module('app', []).run(["MyService",
function (MyService) {
MyService.initListeners();
}
]);
// this is supposed to be in another file
angular.module('app')
.service("MyService", function() {
return {
initListeners: function() {
console.log("working")
}
}
})
}(angular));
The test is this:
(function () {
'use strict';
describe("MyService", function () {
var MyService = null;
beforeEach(module("app"));
beforeEach(inject(function ($injector) {
MyService = $injector.get('MyService');
}));
afterEach(function () {
MyService = null;
});
it("injection works", function () {
expect(true).toBeTruthy(); // throws exception
});
});
}());
I did the test on a jsfiddle.
I see the order of execution with some console messages. The order of execution is the correct, as expected.
My service constructor
before init
MyService.initListeners()
after init
And the two test work correctly:
it("injection works", function () {
expect(true).toBeTruthy(); // throws exception
expect(MyService).toBeDefined();
});
Here is the code: http://jsfiddle.net/jordiburgos/1efvof3k/
It could be your AngularJS version, Jasmine, etc...
When running an angularjs + Jasmine + Karma test, I got following error:
My test script is:
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
it('should create "phones" model with 3 phones', inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', { $scope: scope });
expect(scope.phones.length).toBe(3);
}));
});
});
This code is just a copy from official AngularJS tutorial here:
http://code.angularjs.org/1.2.0-rc.3/docs/tutorial/step_02
Here is part of my karma.conf.js file:
// list of files / patterns to load in the browser
files: [
'js/bower_components/angular/angular.js',
'js/bower_components/angular/ngular-mocks.js',
'js/app/controllers.js',
'test/unit/*.js'
],
The error is PhoneListCtrl not define, but I beleive it is defined and loaded in the above code. What do you think is the problem? Thanks!
Module initialization part is missing in your unit test. You should call module('phonecatApp') before you first time call inject(). Your unit test code in this case should look like:
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(function() {
module('phonecatApp'); // <= initialize module that should be tested
});
it('should create "phones" model with 3 phones', inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', { $scope: scope });
expect(scope.phones.length).toBe(3);
}));
});
});
where phonecatApp is the name of the module where you defined your PhoneListCtrl controller.
Also tutorial you are using is outdated, it is for unstable version of Angular (1.2.0-rc.3). Here is an updated version of the same tutorial for the latest version of Angular: http://docs.angularjs.org/tutorial/step_02
this works for me
describe('addCatControllerTest', function() {
describe('addCatController', function(){
beforeEach(function() {
module('app');
});
beforeEach(inject(function($controller, $rootScope){
$scope = $rootScope.$new();
}));
it('Add Cat Controller test', inject(function($controller) {
var scope = {},
ctrl = $controller('addCatController', { $scope: scope });
expect(scope.title).toBe('Add Cat');
}));
});
});