Angular Test First Workflow
The context
Single page applications are significant part of a modern web. Their scale is increasing from simple tools to large webpages. Because of that, we as developers have to be sure that our apps are working as expected, and that new features we are implementing would not break other parts of our page.
According to W3Thechs, high traffic sites, such as Weather.com, Ford.com are using AngularJS library. The fast-growing popularity of AngularJS in comparison to ember.js and backbone.js is also illustrated in the graph bellow:
In this article we will discuss how to create simple but efficient workflow for writing unit tests with Grunt and AngularJS. The main tools to be used are:
- NPM, Bower - package managers
- Grunt – the JavaScript task runner
- Karma – JavaScript test runner
- Jasmine – JavaScript testing framework
The main goal of this tutorial is to run unit tests automatically during the development, and create a simple test-first workflow.
First of all, let us install few packages with NPM:
npm install grunt grunt-cli grunt-contrib-connect grunt-contrib-watch serve-static --save-dev
These are packages required for our local server. Another important group of packages are those, required for karma
:
npm install grunt-karma jasmine-core karma karma-jasmine karma-phantomjs-launcher --save-dev
After installing packages in your package.json
file you will find a section with devDependencies:
"devDependencies": {
"grunt-cli": "^0.1.13",
"grunt-contrib-connect": "^0.11.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-karma": "^0.12.0",
"jasmine-core": "^2.3.4",
"karma": "^0.13.9",
"karma-jasmine": "^0.3.6",
"karma-phantomjs-launcher": "^0.2.1",
"phantomjs": "^1.9.18",
"serve-static": "^1.10.0"
}
Although we have some packages for Grunt installed, we still need the AngularJS. Now we will install it using Bower.
bower install angular --save-dev
bower install angular-mocks --save-dev
After installing bower componenets, we need to create a Gruntfile.js
– a file with our tasks. If you are not sure how to do this, please have a look at the documentation
Now my file looks as follows:
'use strict';
var serveStatic = require('serve-static');
module.exports = function (grunt) {
grunt.initConfig({
connect: {
options: {
port: 9000,
hostname: 'localhost',
livereload: 35729,
},
livereload: {
options: {
open: true,
base: [
'./app'
],
middleware: function (connect, options, middlewares) {
middlewares.push(connect().use(
'/bower_components',
serveStatic('./bower_components'))
);
return middlewares;
}
}
}
},
watch: {
js: {
files: ['app/*.js', 'app/**/*.js'],
// runs karma after saving any *.js file
// in app folder (app.js or app_tests.js)
tasks: ['karma:unit:run'],
options: {
livereload: '<%= connect.options.livereload %>'
}
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'app/*.*',
'app/**/*.*',
]
}
},
// grunt-karma configuration
karma: {
options: {
// framework installed with package karma-jasmine
frameworks: ['jasmine'],
// runner installed with package karma-phantomjs-launcher
browsers: ['PhantomJS'],
},
unit: {
// runs karma in a child process
background: true,
// keeps karma server running after the test execution
singleRun: false,
runnerPort: 9999,
// files that will be available in PhantomJS
files: [{
src: [
'./bower_components/angular/angular.js',
'./bower_components/angular-mocks/angular-mocks.js',
'./app/scripts/*.js'
]
}]
}
},
});
// Requiered for grunt tasks
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-karma');
grunt.registerTask('live', [
// start karma server
'karma:unit',
// start local server
'connect:livereload',
// listen to changes in app files
'watch'
]);
};
Please have a look on the section karma
. There are two main properties which help us run tests in the background:
background: true,
singleRun: false
Also, we added karma task in watch section for each and every change of *.js files:
tasks: ['karma:unit:run']
The above written code is the karma trigger that runs the tests.
Last but not least, we need to keep both the karma and local servers running. In order to do this, please create a new task:
grunt.registerTask('live', [
// start karma server
'karma:unit',
// start local server
'connect:livereload',
// listen to changes in app files
'watch'
]);
In console, after entering the command grunt live
, these tasks will be executed. The first one is karma:unit
, which starts karma server, the second one – connect-livereload
– starts local server with live reload option. The last one – watch
– is listening to changes in the files.
As soon as we finish the configuration, we create a new app. What we need is app/index.html
, app/scripts/app.js
and app/scripts/app_tests.js
. In this project they are in the app
folder.
index.html
<!doctype html>
<html lang="en" ng-app="AngularUnitFlow">
<head>
<title>Angular Unit test workflow</title>
</head>
<body>
<div ng-controller="MainController"></div>
<script src="/bower_components/angular/angular.js"></script>
<script src="/scripts/app.js"></script>
</body>
</html>
scripts/app.js
angular.module('AngularUnitFlow', []);
angular.module('AngularUnitFlow')
.controller('MainController', ['$scope', function($scope){
// place for new functionality
}]);
scripts/app_tests.js
describe('App', function() {
beforeEach(module('AngularUnitFlow'));
describe('Main Controller', function () {
var scope, controller;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('MainController', { $scope:scope });
}));
it('should be defined', inject(function($controller) {
expect($controller).toBeDefined();
}));
// in this place you need to add new tests
});
});
Folder structure
|- app/
|- scripts/
|- app.js
|- app_tests.js
|- index.html
|- bower.json
|- Gruntfile.js
|- package.json
Run command grunt live
to run the server and add the first test:
it('should has string variable', function() {
expect(scope.testableVariable).toEqual('This is a string');
});
After saving the file in the console you will find:
>> File "app\scripts\app_tests.js" changed.
Running "karma:unit:run" (karma) task
PhantomJS 1.9.8 (Windows 8 0.0.0) App Main Controller should has string variable FAILED
Expected undefined to equal 'This is a string'.
at app/scripts/app_tests.js:17
PhantomJS 1.9.8 (Windows 8 0.0.0): Executed 2 of 2 (1 FAILED) (0 secs / 0.009 secsPhantomJS 1.9.8 (Windows 8 0.0.0): Executed 2 of 2 (1 FAILED) (0.001 secs / 0.009 secs)
Warning: Task "karma:unit:run" failed. Use --force to continue.
Aborted due to warnings.
The test we have written failed, but this is fine. Now, we will write code for our test. In app.js
you need to add a new variable in MainController
controller:
$scope.testableVariable = 'This is a string';
Now please save the file and have a look at the console window:
>> File "app\scripts\app.js" changed.
Running "karma:unit:run" (karma) task
PhantomJS 1.9.8 (Windows 8 0.0.0): Executed 2 of 2 SUCCESS (0 secs / 0.007 secs)
Done, without errors.
Source code
As you can see, now we have our project configured for test-first development. The whole project is available here. Please download the archive, unpack to any folder and install there the packages with commands: npm install
and bower install
. Now start the project with command grunt live
and… have fun!