Email iconarrow-down-circleGroup 8Path 3arrow-rightGroup 4Combined Shapearrow-rightGroup 4Combined ShapeUntitled 2Untitled 2Path 3ozFill 166crosscupcake-icondribbble iconGroupPage 1GitHamburgerPage 1Page 1LinkedInOval 1Page 1Email iconphone iconPodcast ctaPodcast ctaPodcastpushpinblog icon copy 2 + Bitmap Copy 2Fill 1medal copy 3Group 7twitter icontwitter iconPage 1

Overview

The end product of this tutorial series is available at www.podcasts.uvd.co.uk.

All the code for this tutorial is publicly available here on Github. I’m updating the repository with tags that correspond to the parts of this tutorial series.


I started playing around with Angular two years ago at home for a personal project. Having previously used KnockoutJS as my go-to JS library, Angular seemed like a great progressive step forward and I quickly became a huge fan of it. When we started using it as a company for a project with a far larger scale, I started to experience the infamous Angular learning-curve:

AngularJS Learning Curve
Credit to Ben Nadel (bennadel.com)

But now after two solid years and around 5 commercial Angular success stories under my belt, I finally feel that I’ve conquered that curve.

The aim of this series of tutorials is to share some of this knowledge and specifically demonstrate how I develop Angular applications following the BDD (Behaviour Driven Development) philosophies that we champion at UVD.  I’ll be honest here, writing tests before writing any code is hard / impossible if you don’t know the framework very well, and at the beginning of my Angular journey it was nigh-on impossible, so don’t be disparaged if some of this seems a little confusing. That being said, all of the following tutorial was written in parallel to the code, in a linear process, in order to demonstrate the thought process I have when developing in BDD. A basic knowledge of AngularJS and unit-testing in general is recommended for this tutorial.

Each section of the tutorial will be a github tag, so the first section of this tutorial is tagged as part-1 and is available here.

Although some people may be interested in how I set up the project with all of our dependencies, to some people this may be familiar and unnecessary. For those people I urge you to skip straight to part 2, where the fun actually begins and simply skip to the part-2 tag in the repository.

Prerequisites


Chrome

  • This tutorial uses chrome for running unit and E2E tests.

Git

Node and NPM

Yeoman (Yo/Grunt/Bower)

  • If you are going to be following along with part 1 of this tutorial you’ll also need to install the npm module yo globally (npm install -g yo)

Running the app during development

After scaffolding your application you can serve the application by running grunt serve.

Run unit tests with grunt test.

And run E2E tests with grunt e2e.

1. Setting Up


We’re going to use Yo to scaffold out the skeleton of our framework. You can read more about Yo at http://yeoman.io/.

To run yo simply run the command yo. And you’ll be presented with the following options:


[?] What would you like to do? (Use arrow keys)
❯ Install a generator
  Find some help
    Get me out of here!

The first thing we need to do is install the Angular generator. You can do this either by installing via npm with npm install -g generator-angular or by searching for it within yo by choosing the Install a generator option and searching for angular:


[?] What would you like to do? Install a generator
[?] Search NPM for generators: angular
[?] Here's what I found. Install one?
    dbox-generator-angular
  ❯ generator-angular
    generator-angular-axiom
    generator-angular-beego
    generator-angular-bootstrap
    generator-angular-bootstrap-less
    generator-angular-brent
(Move up and down to reveal more choices)

Once the generator is installed it will appear in the root menu:


[?] What would you like to do? (Use arrow keys)
❯ Run the Angular generator (0.9.8)
  ...

Running the generator in a new directory will begin the process of creating your project scaffolding. For this tutorial I’ve selected the following options but you can customise to suit your tastes. This tutorial will be using the restangular and ui-router libraries so the use of ngRoute and ngResource will definitely not be needed.


[?] What would you like to do? Run the Angular generator (0.9.8)

Make sure you're in the directory you want to scaffold into.
This generator can also be run with: yo angular

     _-----_
    |       |    .--------------------------.
    |--(o)--|    |    Welcome to Yeoman,    |
   `---------´   |   ladies and gentlemen!  |
    ( _´U`_ )    '--------------------------'
    /___A___
     |  ~  |

[?] Would you like to use Sass (with Compass)? Yes
[?] Would you like to include Bootstrap? Yes
[?] Would you like to use the Sass version of Bootstrap? Yes

You should then be asked which modules to include with your new application. For this application we won’t be using any of the recommended modules so de-select them all and press space to continue.

If all goes well you should not have a scaffolded angular app. To run it via a node server call grunt serve.

Customising the set-up

package.json

Replace the contents of the package.json file with the following, and run npm install. We’ll be using everything in here at some stage or another throughout the tutorial.

{
"name": "angularpodcasttutorial",
"version": "0.0.0",
"dependencies": {
"grunt-ng-constant": "^1.0.0"
},
"devDependencies": {
"chai": "^1.9.1",
"chai-as-promised": "^4.1.1",
"cucumber": "latest",
"grunt": "^0.4.1",
"grunt-angular-templates": "^0.5.6",
"grunt-autoprefixer": "^0.7.3",
"grunt-concurrent": "^0.5.0",
"grunt-contrib-clean": "^0.5.0",
"grunt-contrib-compass": "^0.7.2",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-connect": "^0.7.1",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.9.0",
"grunt-contrib-htmlmin": "^0.3.0",
"grunt-contrib-imagemin": "^0.7.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.4.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-cucumber": "^0.2.3",
"grunt-filerev": "^0.2.1",
"grunt-google-cdn": "^0.4.0",
"grunt-karma": "^0.8.3",
"grunt-newer": "^0.7.0",
"grunt-ngmin": "^0.0.3",
"grunt-shell": "~0.6.4",
"grunt-shell-spawn": "~0.3.0",
"grunt-svgmin": "^0.4.0",
"grunt-usemin": "^2.1.1",
"grunt-wiredep": "^1.7.0",
"jshint-stylish": "^0.2.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^0.1.4",
"karma-fixture": "^0.2.1-1",
"karma-html2js-preprocessor": "^0.1.0",
"karma-json-fixtures-preprocessor": "0.0.1",
"karma-mocha": "^0.1.3",
"karma-ng-html2js-preprocessor": "^0.1.2",
"karma-phantomjs-launcher": "^0.1.4",
"karma-sinon-chai": "^0.1.6",
"load-grunt-tasks": "^0.4.0",
"lodash": "^2.4.1",
"mocha": "^1.20.1",
"protractor": "~0.24.1",
"q": "^1.0.1",
"robohydra": "^0.4.0",
"selenium-webdriver": "~2.42.1",
"sinon": "^1.10.2",
"sinon-chai": "^2.5.0",
"time-grunt": "^0.3.1"
},
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"test": "grunt test",
"postinstall": "./node_modules/protractor/bin/webdriver-manager update"
}
}

bower.json

As mentioned earlier we’ll be shunning Angular’s native ngResource and ngRoute modules in favour of some more powerful alternatives: Restangular and ui-router. To install these two components simply use bower: bower install restangular --save && bower install ui-router --save.

Our complete bower.json file should look something like this (please note that angular version 1.2.* was used at the time this tutorial was created):

{
"name": "uvd-angular-podcast-tutorial",
"version": "0.0.0",
"dependencies": {
"angular": "1.2.*",
"bootstrap-sass-official": "^3.2.0",
"restangular": "~1.4.0",
"ui-router": "~0.2.13"
},
"devDependencies": {
"angular-mocks": "1.2.*"
},
"appPath": "app",
"moduleName": "uvdAngularPodcastTutorialApp"
}

Gruntfile.js

Next we need to make some changes to Gruntfile.js.

Add the selenium path to the top of the Gruntfile:

var path = require('path'),
seleniumPath = path.resolve('./node_modules/protractor/selenium');

Under the connect heading add the e2e section:

...
e2e: {
options: {
open: false,
port: 9003,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect.static('e2e'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
...

Add the following shell section with the protractor task:

shell: {
protractor: {
command: './node_modules/protractor/bin/protractor protractor-conf.js'
}
},

And the following additional tasks to the e2e heading:

env: {
test: {
PATH: seleniumPath + ':process.env.PATH'
}
},
cucumberjs: {
src: 'features/*.feature',
options: {
format: 'pretty',
steps: 'features/step_definitions',
support: 'features/support'
}
}

protractor-conf.js

Create the protractor-conf.js file in the root of the project.

We use the following configuration, which tends animations to speed up our E2E tests:

// The location of the selenium standalone server .jar file, relative
// to the location of this config. If no other method of starting selenium
// is found, this will default to
// node_modules/protractor/selenium/selenium-server...
// seleniumServerJar: null,
// The port to start the selenium server on, or null if the server should
// find its own unused port.
// seleniumPort: null,
// Chromedriver location is used to help the selenium standalone server
// find chromedriver. This will be passed to the selenium jar as
// the system property webdriver.chrome.driver. If null, selenium will
// attempt to find chromedriver using PATH.
// chromeDriver: './selenium/chromedriver',
// If true, only chromedriver will be started, not a standalone selenium.
// Tests for browsers other than chrome will not run.
chromeOnly: false,
// Additional command line options to pass to selenium. For example,
// if you need to change the browser timeout, use
// seleniumArgs: ['-browserTimeout=60'],
seleniumArgs: [],

// If sauceUser and sauceKey are specified, seleniumServerJar will be ignored.
// The tests will be run remotely using SauceLabs.
// sauceUser: null,
// sauceKey: null,

// The address of a running selenium server. If specified, Protractor will
// connect to an already running instance of selenium. This usually looks like
// seleniumAddress: 'http://localhost:4444/wd/hub'
seleniumAddress: null,

// The timeout for each script run on the browser. This should be longer
// than the maximum time your application needs to stabilize between tasks.
allScriptsTimeout: 11000,

// ----- What tests to run -----
//
// Spec patterns are relative to the location of this config.
specs: [
'features/*.feature',
// 'features/user_archives_a_user_story.feature'
],

// Patterns to exclude.
exclude: [],

// Alternatively, suites may be used. When run without a command line parameter,
// all suites will run. If run with --suite=smoke, only the patterns matched
// by that suite will run.
// suites: {
// smoke: 'spec/smoketests/*.js',
// full: 'spec/*.js'
// },

// Maximum number of total browser sessions to run. Tests are queued in
// sequence if number of browser sessions is limited by this parameter.
// Use a number less than 1 to denote unlimited. Default is unlimited.
maxSessions: -1,

// ----- Capabilities to be passed to the webdriver instance ----
//
// For a list of available capabilities, see
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
// and
// https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
// Additionally, you may specify count, shardTestFiles, and maxInstances.
capabilities: {
browserName: 'chrome',

// Number of times to run this set of capabilities (in parallel, unless
// limited by maxSessions). Default is 1.
count: 1,

// If this is set to be true, specs will be sharded by file (i.e. all
// files to be run by this set of capabilities will run in parallel).
// Default is false.
shardTestFiles: false,

// Maximum number of browser instances that can run in parallel for this
// set of capabilities. This is only needed if shardTestFiles is true.
// Default is 1.
maxInstances: 1
},

// If you would like to run more than one instance of webdriver on the same
// tests, use multiCapabilities, which takes an array of capabilities.
// If this is specified, capabilities will be ignored.
multiCapabilities: [],

// ----- More information for your tests ----
//
// A base URL for your application under test. Calls to protractor.get()
// with relative paths will be prepended with this.
baseUrl: 'http://127.0.0.1:9003',

// Selector for the element housing the angular app - this defaults to
// body, but is necessary if ng-app is on a descendant of <body>
rootElement: 'body',

// A callback function called once protractor is ready and available, and
// before the specs are executed
// You can specify a file containing code to run by setting onPrepare to
// the filename string.
onPrepare: function() {
// Disable animations so e2e tests run more quickly
'use strict';
var width = 1280;
var height = 800;
browser.driver.manage().window().setSize(width, height);
var disableNgAnimate = function() {

jQuery.fn.animate = function(){ return this; };

angular.module('disableNgAnimate', []).run(function($animate) {
$animate.enabled(false);
});

};

browser.addMockModule('disableNgAnimate', disableNgAnimate);

},

// The params object will be passed directly to the protractor instance,
// and can be accessed from your test. It is an arbitrary object and can
// contain anything you may need in your test.
// This can be changed via the command line as:
// --params.login.user 'Joe'
// params: {
// login: {
// user: 'Jane',
// password: '1234'
// }
// },

// ----- The test framework -----
//
// Jasmine and Cucumber are fully supported as a test and assertion framework.
// Mocha has limited beta support. You will need to include your own
// assertion framework if working with mocha.

framework: 'cucumber',

// ----- Options to be passed to cucumber -----
cucumberOpts: {
// Require files before executing the features.
require: ['features/support/hooks.js', 'features/step_definitions/*.js'],
// Only execute the features or scenarios with tags matching @dev.
// This may be an array of sttags: '@dev',rings to specify multiple tags to include.
//
// How to format features (default: progress)
format: 'pretty'
},

// ----- The cleanup step -----
//
// A callback function called once the tests have finished running and
// the webdriver instance has been shut down. It is passed the exit code
// (0 if the tests passed or 1 if not).
onCleanUp: function() {}
};

Karma.conf.js

By default the generator-angular expects tests to use Jasmine, but in this tutorial we’ll be using a combination of Mocha/Sinon and Chai. Because of this we’ll have to edit out the karma.conf.js file located in the tests directory.

Replace the contents of karma.conf.js with the following set-up which is focused on our testing stack:

'use strict';

// Karma configuration
// http://karma-runner.github.io/0.12/config/configuration-file.html
// Generated on 2014-09-22 using
// generator-karma 0.8.2

module.exports = function (config) {
config.set({
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,

// base path, that will be used to resolve files and exclude
basePath: '../',

preprocessors: {
'app/views/**/*.html': ['ng-html2js'],
'app/template/**/*.html': ['ng-html2js']
},

// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['mocha', 'sinon-chai'],

// list of files / patterns to load in the browser
files: [
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
'bower_components/ui-router/release/angular-ui-router.js',
'bower_components/lodash/dist/lodash.compat.js',
'bower_components/restangular/dist/restangular.js',
'.tmp/scripts/config.js',
'app/scripts/**/*.js',
'.tmp/scripts/templates.js',
'test/mock/**/*.js',
'test/spec/**/*.js'
],

// list of files / patterns to exclude
exclude: [],

// web server port
port: 8080,

// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: [
'Chrome'
],

// Which plugins to enable
plugins: [
'karma-chrome-launcher',
'karma-mocha',
'karma-sinon-chai'
],

// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false,

colors: true,

// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO

// Uncomment the following lines if you are using grunt's server to run the tests
// proxies: {
// '/': 'http://localhost:9000/'
// },
// URL root prevent conflicts with the site root
// urlRoot: '_karma_'
});
};

We should now have our project set up and we’re ready to start writing our first high-level E2E tests.

Code created available here: https://github.com/uvd/podcast-application/tree/part-1

Continue to part 2.

Share: