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

We’ll be using the gherkin language to define readable feature files that comprise the acceptance criteria of specific features.

Gherkin is a Business Readable, Domain Specific Language created specifically for behaviour descriptions. It gives you the ability to remove logic details from behaviour tests. Gherkin serves as your project’s documentation as well as your project’s automated tests.

Our user story is for a user to be able to search for a particular podcast, so we’re going to create a human-readable gherkin feature file for the user story. Generally these features will define the acceptance criteria for a particular user story, and can be written in conjunction with the client or domain expert. We’re not going to be quite so strict with this little personal project, so we’ll just define a few high-level scenarios.

Add the following feature file to a new directory features in the root of the project.

user-searches-for-a-podcast.feature

Feature: User searches for a podcast

    As a user of the podcast application
    I want to search for podcasts that I know
    In order to be able to find episodes to listen to

    Background:
        Given that I am on the podcast search page

    Scenario: User finds positive results for their search query
        When I search for the podcast "JavaScript"
        Then I should see the following search results:
            | Title                   | Feed                                             |
            | The Javascript Show     | http://feeds.feedburner.com/the-javascript-show  |
            | JavaScript Jabber       | http://feeds.feedwrench.com/JavaScriptJabber.rss |
         And I should not see an alert

    Scenario: User finds no results for their podcast query
        When I search for the podcast "07yfabOAgiuae"
        Then I should see no search results
        And I should see the alert "No search results"

If we run the grunt e2e command, selenium2 should open a new instance of Chrome ready to perform the steps that we outline in the feature file, except we’ve yet to write the step definitions.

Add the following directories to the features directory.

/features
    /user-searches-for-a-podcast.feature
    /step_definitions
    /page_object
    /support

world.js

To support our step definitions we’ll be creating a world.js file in the support directory which will contain access to some helper libraries. I’ve also added some functions I’ve created that help with getting repeating data out of pages that we’ll be using later. You don’t need to understand how these work too much but if you’d like to know more then just ask me in the comments section.

var World = function World(callback) {

    'use strict';

    var chai = require('chai');
    var chaiAsPromised = require('chai-as-promised');

    chai.use(chaiAsPromised);

    global.expect = chai.expect;
    global.Q = require('Q');
    global._ = require('lodash');

    /**
     *
     * @param results
     * @param column
     * @returns {!Promise.<RESULT>|*|!webdriver.promise.Promise.<R>}
     */
    global.getRepeaterColumn = function getRepeaterColumn(results, column) {
        return element.all(results.column(column)).then(function (c) {
            return Q.all(c.map(function (e) {
                return e.getText().then(function (text) {
                    return text;
                });
            }));
        });
    };

    /**
     *
     * @param results
     * @param table
     * @returns {*|!Promise.<RESULT>|webdriver.promise.Promise|Rx.IPromise<R>|!webdriver.promise.Promise.<R>}
     */
    global.getRepeaterColumns = function getRepeaterColumns(results, columns) {

        return Q.all(columns.map(function (e) {
                return getRepeaterColumn(results, e);
            }))
            .then(function (resolvedColumns) {
                return resolvedColumns.map(function (e, i) {
                    return _.pluck(resolvedColumns, i);
                });
            });
    };

    /**
     *
     * @param results
     * @returns {Rx.Observable<number>|!webdriver.promise.Promise}
     */
    global.getRepeaterCount = function getRepeaterCount(results) {
        return element.all(results).count();
    };

    callback();

};

module.exports.World = World;

Add the following task to your Gruntfile, this will allow us to run our e2e tests:

grunt.registerTask('e2e', [
    'clean:server',
    'wiredep',
    'concurrent:server',
    'autoprefixer',
    'connect:e2e',
    'shell:protractor'
]);

Now that we have our feature file we can run it with grunt e2e. Protractor and selenium will control a new instance of the browser, but we have to instruct it. The step definitions that haven’t been implemented will be in the command line for us to use.

We’re now going to start implementing the step definitions.

Add a new javascript file user-searches-for-a-podcast.js to the step definitions directory and paste in the empty step definitions from the command line in a new empty module:

function UserSearchesForAPodcast () {

    this.Given(/^that I am on the podcast search page$/, function (callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.When(/^I search for the podcast '([^"]*)'$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.Then(/^I should see the following search results:$/, function (table, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.Then(/^I should see the message '([^"]*)'$/, function (arg1, callback) {
        // Write code here that turns the phrase above into concrete actions
        callback.pending();
    });

    this.Then(/^I should see no search results$/, function (callback) {
          // Write code here that turns the phrase above into concrete actions
          callback.pending();
    });
}

module.exports = UserSearchesForAPodcast;

We can now start implementing the first step of our feature:

this.Given(/^that I am on the podcast search page$/, function (callback) {
    browser.get('#/search').then(callback);
});

The E2E tests have access to the global browser object, which is how we can control the browser. This definition says ‘Go to “#/search” and when that has completed go to the next step’

Now in true BDD fashion it’s time to go inside and create some tests around the creation of a new route.

state.js

We’ll be using the ui-router library as a drop-in replacement for angular ngRoute. We need to add this as an angular dependency:

'use strict';

/**
 * @ngdoc overview
 * @name angularPodcastTutorialApp
 * @description
 * # angularPodcastTutorialApp
 *
 * Main module of the application.
 */
angular
  .module('angularPodcastTutorialApp', [
        'ui.router'
    ]);

Let’s start creating a state.js file that will hold the routes of our application. First, we’ll set up our test suite like so in test/spec/state.js.

spec/state.js

First we need to set up our tests:

'use strict';

describe('State', function () {

    var $rootScope,
        $state;

    beforeEach(function () {

        module('angularPodcastTutorialApp');

        inject(function (_$rootScope_, _$state_) {
            $rootScope = _$rootScope_;
            $state = _$state_;
        });

    });

});

This code inititates our angular module and allows us to access $rootScope and $state in our individual tests. We can test the state like this:

it('should have a search state', function () {
    expect($state.href('search')).to.equal('#/search');
    $state.go('search');
    $rootScope.$digest();
    expect($state.current.name).to.equal('search');
});

This tests two things:

  1. The route named search has the href #/search
  2. The route named search is accessible (we actually attempt to go to the route and assert that we are currently on it

Note that we must call the a $digest on the $rootScope for us to actually start a digest cycle and change state. See here for more information.

Note that yeoman bootstraps our application with one dummy controller and one dummy spec for this controller. Feel free to delete these!

Chrome 38.0.2125 (Mac OS X 10.8.3) State should have a search state FAILED
    AssertionError: expected null to equal '#/search'

This is the infamous TDD red in the red->green->refactor cycle; it means we can now start implementing the search state!

state.js

'use strict';

/**
 *
 * @param $stateProvider
 * @param $urlRouterProvider
 * @constructor
 */
function State ($stateProvider, $urlRouterProvider) {

    $stateProvider.state('search', {
        url: '/search',
        templateUrl: 'views/search.html'
    });

    $urlRouterProvider.otherwise('/search');

}

/**
 * @ngdoc state
 * @name angularPodcastTutorialApp
 * @description
 * # angularPodcastTutorialApp
 *
 * States of the application.
 */
angular.module('angularPodcastTutorialApp').config(State);

views/search.html

We need to now add our search template,  you can copy the markup from here.

When we run the tests now we’ll see that we have a new problem:

Chrome 38.0.2125 (Mac OS X 10.8.3) State should have a search state FAILED
        Error: Unexpected request: GET views/search.html
        No more request expected

Because we’re using a template from an external HTML file, our angular application is trying to get the template via an ajax request when we change to that state. We could use $httpBackend here to tell the test what to return, but this will get tedious in the long run as we add more view files to states or directives. Instead we opt for the $templateCache approach.

Install ngTemplates with npm install grunt-angular-templates –save

Add the following section to your Gruntfile:

ngtemplates: {
    myapp: {
        options: {
            base: 'views',        // $templateCache ID will be relative to this folder
            module: 'angularPodcastTutorialApp',               // (Optional) The module the templates will be added to
            htmlmin: {
                collapseBooleanAttributes: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeComments: true, // Only if you don't use comment directives!
                removeEmptyAttributes: true,
                removeRedundantAttributes: true,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true
            }
        },
        cwd: '<%= yeoman.app %>',
        src: [
            'views/*.html',
            'views/**/*.html',
            'views/**/**/*.html',
            'template/**/*.html'

        ],
        dest: '.tmp/scripts/templates.js'
    }
},

Then add the ngtemplates task to your test run:

grunt.registerTask('test', [
    'clean:server',
    'wiredep',
    'concurrent:test',
    'autoprefixer',
    'ngtemplates',
    'connect:test',
    'karma'
]);

and run grunt test (or restart if you turned singleRun to false) one more to find:

Chrome 38.0.2125 (Mac OS X 10.8.3): Executed 1 of 1 SUCCESS (0.067 secs / 0.011 secs)

Success

What we’ve done is told Grunt to compile all of the templates for us automatically and add them to a .tmp/scripts/templates.js. We’ve already added this file to our karma.conf.js file so no ajax calls have to be made. The application retrieves the template from the $templateCache automatically. Hurrah!

Now that we have the state working and tests to surround it we can start creating the search API. For this we will be using Restangular. Add Restangular to your app.js:

angular
  .module('uvdAngularPodcastTutorialApp', [
        'ui.router',
        'restangular'
    ]);

We can use yeoman to bootstrap the process of creating a new module. Simply type yo angular:service itunes-api in the terminal. Yeoman will create the service and a corresponding spec file:

create app/scripts/services/itunes-api.js
create test/spec/services/itunes-api.js

Update your itunes-api spec to be the following:

'use strict';

describe('Service: ItunesApi', function () {

    // load the service's module
    beforeEach(module('angularPodcastTutorialApp'));

    // instantiate service
    var Restangular, callback, configurer;

    beforeEach(inject(function (_Restangular_) {

        Restangular = _Restangular_;

        configurer = {
            setJsonp: sinon.spy(),
            setDefaultRequestParams: sinon.spy(),
            setBaseUrl: sinon.spy(),
            setResponseInterceptor: sinon.spy()
        };

        sinon.stub(Restangular, 'withConfig', function (_callback_) {
            callback = _callback_;
            return true;
        });

    }));
});

You may want to do some reading of the Restangular documentation to get an idea of what’s going on in the next section! Essentially we’re creating and configuring a Restangular instance to query the iTunes search API.

This will be the foundation for our tests on the Restangular set up. We stub out the withConfig method with sinon, which is usually passed a callback in our actual code. We can then execute our callback with a dummy RestangularConfigurer and check that functions on it have been called with the data we expect. To check that it works, let’s create our first test:

describe('Basic configuration', function () {

    it('should return Restangular with config', inject(function (ItunesApi) {
        expect(Restangular.withConfig).to.have.been.called;
    }));
});

Note that we inject ItunesApi in the test definition rather than the beforeEach because otherwise we won’t be able to set up our stub in time.

Which should give us our first failing test:

Chrome 38.0.2125 (Mac OS X 10.8.3) Service: ItunesApi Basic configuration should return Restangular with config FAILED
    AssertionError: expected withConfig to have been called at least once, but it was never called

Great, so now we can write some code to make this test pass:

'use strict';

function ItunesApi(Restangular) {

    return Restangular.withConfig(function (RestangularConfigurer) {

    });

}

/**
 * @ngdoc service
 * @name uvdAngularPodcastTutorialApp.ItunesApi
 * @description
 * # ItunesApi
 * Service in the uvdAngularPodcastTutorialApp.
 */
angular.module('uvdAngularPodcastTutorialApp')
    .service('ItunesApi', ItunesApi);

Et voila:

    Chrome 38.0.2125 (Mac OS X 10.8.3): Executed 2 of 2 SUCCESS (0.094 secs / 0.01 secs)

Let’s add some more tests for our ItunesApi configuration. We know we must set the base url to be https://itunes.apple.com/ so let’s write a test for that:

    it('should set the base url to that of the itunes API', inject(function (ItunesApi) {
        callback(configurer);
        expect(configurer.setBaseUrl).to.have.been.calledWithExactly('https://itunes.apple.com/');
    }));

What we do here is actually call the callback we’ve supplied to the restangular withConfig method with our stubby version of the configurer.

Chrome 38.0.2125 (Mac OS X 10.8.3) Service: ItunesApi Basic configuration should set the base url to that specified in a constant FAILED
    AssertionError: expected spy to have been called with exact arguments https://itunes.apple.com/

And let’s make it pass:

        return Restangular.withConfig(function (RestangularConfigurer) {
            RestangularConfigurer.setBaseUrl('https://itunes.apple.com/');
        });

Success!

Chrome 38.0.2125 (Mac OS X 10.8.3): Executed 3 of 3 SUCCESS (0.089 secs / 0.01 secs)

Now we’ll add some more tests to check that we have set up the JsonP configuration correctly and that we’ve set the correct media type (which is just a query param):

    it('should set Jsonp to true', inject(function (ItunesApi) {
        callback(configurer);
        expect(configurer.setJsonp).to.have.been.calledWithExactly(true);
    }));

    it('should set the default request params', inject(function (ItunesApi) {
        callback(configurer);
        expect(configurer.setDefaultRequestParams).to.have.been.calledWithExactly('jsonp', { callback: 'JSON_CALLBACK', media: 'podcast' });
    }));

And now we’ll make them pass:

RestangularConfigurer.setJsonp(true);
RestangularConfigurer.setDefaultRequestParams('jsonp', { callback: 'JSON_CALLBACK', media: 'podcast' });

Finally, if we look at the format that the ItunesApi returns for its search results they are returned in the following format:

{
     "resultCount":1,
     "results": [

     ]
}

However Restangular (and us for that matter) expects just an array of objects if we use the getList command. We can use a response interceptor to get this working by transforming the response in to the format that we’re expecting. First, let’s create a test.

  1. Add the setResponseInterceptor spy to our stubbed out configurer.
  2. Check that if we have called the interceptor callback function provided with the correct parameters, the interceptor will just return an array.

Test:

    it('should add a response interceptor for itunes search results', inject(function (ItunesApi) {
        var interceptor;
        var dummyResponse = {
            resultCount: 2,
            results: [
                { name: 'Podcast 1' },
                { name: 'Podcast 2' }
            ]
        };
        var interceptorResponse;
        callback(configurer);
        expect(configurer.setResponseInterceptor).to.have.been.calledOnce;
        //Get the callback we have added as a response intereceptor
        interceptor = configurer.setResponseInterceptor.getCall(0).args[0];
        interceptorResponse = interceptor(dummyResponse, 'getList', 'search', 'http://itunes.apple.com/search?term=test', {}, {});
        expect(interceptorResponse).to.deep.equal(dummyResponse.results);
    }));

And corresponding code:

    /**
     * If search results, only return the results.
     *
     * @param data          the response data
     * @param operation
     * @param what
     * @returns Array
     */
    function searchResponseInterceptor (data, operation, what) {

        if (operation === 'getList' && what === 'search') {
            return data.results;
        }

        return data;
    }

    return Restangular.withConfig(function (RestangularConfigurer) {
        RestangularConfigurer.setBaseUrl('https://itunes.apple.com/');
        RestangularConfigurer.setJsonp(true);
        RestangularConfigurer.setDefaultRequestParams('jsonp', { callback: 'JSON_CALLBACK', media: 'podcast' });
        RestangularConfigurer.setResponseInterceptor(searchResponseInterceptor);
    });

Congratulations, we’ve successfully configured Itunes search results for Restangular in a BDD style!

state.js

We’re going to use a resolve function in order to allow our search results to be injected directly into our controller, this de-couples our controllers from our APIs. Controllers shouldn’t care or even know how to get data, they should just know that they need certain data. We’ll need to update ourstate spec to reflect this, so let’s start writing a test.

Inject our newly created ItunesApi into our state test as-well as angular native $injector and stub out the the API in our beforeEach block like so:

describe('State', function () {

    var $rootScope,
        $state,
        $injector,
        ItunesApi,

        searchResults,
        searchStub;

    beforeEach(function () {

        module('uvdAngularPodcastTutorialApp');

        searchResults = [
            { name: 'Dummy result 1' },
            { name: 'Dummy result 2' }
        ];

        searchStub = {
            getList: function () {}
        };

        sinon.stub(searchStub, 'getList', function () {
            return searchResults;
        });

        inject(function (_$rootScope_, _$state_, _$injector_, _ItunesApi_) {
            $rootScope = _$rootScope_;
            $state = _$state_;
            $injector = _$injector_;
            ItunesApi = _ItunesApi_;

            sinon.stub(ItunesApi, 'all', function () {
                return searchStub;
            });

        });

    });
    ...

And update the search test so that it looks like this:

it('should have a search state', function () {
    expect($state.href('search')).to.equal('#/search');
    $state.go('search', { term: 'Angular Podcast' });
    $rootScope.$digest();
    expect($state.current.name).to.equal('search');
    expect(ItunesApi.all).to.have.been.calledOnce.and.calledWithExactly('search');
    expect(searchStub.getList).to.have.been.calledOnce.and.calledWithExactly({ term: 'Angular Podcast' });
    expect($injector.invoke($state.current.resolve.results)).to.equal(dummyResults);
});

Update our actual state to have the term query param:

$stateProvider.state('search', {
    url: '/search?term',
    templateUrl: 'views/search.html'
});

We should not have a red failing test because we haven’t actually implemented the resolve function. Infact we don’t even have the controller yet, so lets get to it.

controllers/search.js

Now we can (finally) start creating our controller. Lets use yeoman again yo angular:controller search. Remove the test that ‘yo’ creates for us and update the bootstrapped search controller to conform to best practices:

'use strict';

/**
 *
 * @constructor
 */
function SearchCtrl () {

}

/**
 * @ngdoc function
 * @name uvdAngularPodcastTutorialApp.controller:SearchCtrl
 * @description
 * # SearchCtrl
 * Controller of the uvdAngularPodcastTutorialApp
 */
angular.module('uvdAngularPodcastTutorialApp')
    .controller('SearchCtrl', SearchCtrl);

We can now create our resolve. The way we’ll create resolves is by putting them in the javascript file with the controller that expects it. This has a plethora of benefits. See: Todd Motto’s angular style guide for more information.

Add the resolve function to the controller:

/**
 * Get the results
 *
 * @type {{results: results}}
 */
SearchCtrl.resolve = {

    results: function ($stateParams, ItunesApi) {
        return ItunesApi.all('search').getList({ term: $stateParams.term });
    }

};

Our test should now look like this:

    AssertionError: expected all to have been called exactly once, but it was called 0 times

But when we update the state to look like this:

$stateProvider.state('search', {
    url: '/search?term',
    templateUrl: 'views/search.html',
    controller: 'SearchCtrl',
    controllerAs: 'searchCtrl',
    resolve: SearchCtrl.resolve
});

We should see it change to:

    Chrome 38.0.2125 (Mac OS X 10.8.3): Executed 6 of 6 SUCCESS (0.109 secs / 0.014 secs)

Awesome. We’ve developed our whole resolve without ever opening a browser!

Let’s finish off this section of the tutorial by just displaying the results on the page. Let’s update our search controller test (please note the controllerAs syntax use):

'use strict';

describe('Controller: SearchCtrl', function () {

    // load the controller's module
    beforeEach(module('uvdAngularPodcastTutorialApp'));

    var SearchCtrl,
        scope,
        results;

    // Initialize the controller and a mock scope
    beforeEach(inject(function ($controller, $rootScope) {

        results = [
            { name: 'One' },
            { name: 'Two' }
        ];

        scope = $rootScope.$new();
        SearchCtrl = $controller('SearchCtrl as searchCtrl', {
            $scope: scope,
            results: results
        });

    }));

    it('should set the results on to scope', function () {
        expect(scope.searchCtrl.results).to.equal(results);
    });

});

And update the controller:

/**
 *
 * @param results
 * @constructor
 */
function SearchCtrl(results) {
    this.results = results;
}

And the template:

<h2 class="page-header">Search Results</h2>

<div id="search-error-message" ng-if="searchCtrl.results.length === 0">
    No search results
</div>

<ul class="media-list">
    <li class="media" ng-repeat="podcast in searchCtrl.results">
        <a class="pull-left" href="#">
            <img class="media-object" ng-src="{{ podcast.artworkUrl100 }}">
        </a>
        <div class="media-body">
            <h4 class="media-heading">{{ podcast.collectionName }}</h4>
            <a ng-href="{{ podcast.feedUrl }}">{{ podcast.feedUrl }}</a>
            <a class="btn btn-default btn-block" ui-sref="podcast({ feed: podcast.feedUrl })">View episodes</a>
        </div>
    </li>
</ul>

Now if we update our index.html, making sure we include all of the scripts we need:

<!doctype html>
<html class="no-js">
<head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
    <!-- build:css(.) styles/vendor.css -->
    <!-- bower:css -->
    <!-- endbower -->
    <!-- endbuild -->
    <!-- build:css(.tmp) styles/main.css -->
    <link rel="stylesheet" href="styles/main.css">
    <!-- endbuild -->
</head>
<body ng-app="uvdAngularPodcastTutorialApp">

<!-- Add your site or application content here -->
<div class="container-fluid" ui-view></div>

<!-- build:js(.) scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js"></script>
<script src="bower_components/lodash/dist/lodash.compat.js"></script>
<script src="bower_components/restangular/dist/restangular.js"></script>
<script src="bower_components/ui-router/release/angular-ui-router.js"></script>
<!-- endbower -->
<!-- endbuild -->

<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<script src="scripts/templates.js"></script>
<script src="scripts/state.js"></script>
<script src="scripts/services/itunes-api.js"></script>
<script src="scripts/controllers/search.js"></script>
<!-- endbuild -->
</body>
</html>

You should now be able to http://localhost:9000/#/search?term=javascript and see search results.

* It’s a good idea here to add the ngtemplates task to your Grunt serve task *

Also note that when you run grunt serve, if you’re not running karma in singleRun mode it will temporarily break the tests as the templates file in .tmp will be removed and re-created. To get around this, use another directory for your templates file that won’t be wiped.

directives/uvd-search-form.js

Let’s start creating the search form. Start by boostrapping a new directive with yo yo angular:directive uvd-search-form.

Note again yo will try and create a dummy unit test for us here, which is geared up for Jasmine, so be sure to delete this test.

In order to make our directive easier to test and to allow our functionality to be used elsewhere, we’ll be skipping like link function for now and instead using a controller with our directive, so let’s create that too with yo yo angular:controller search-form

Let’s start creating the test. We’ll be using ui router $state module to change state to that of our search results:

'use strict';

describe('Controller: SearchformCtrl', function () {

    // load the controller's module
    beforeEach(module('uvdAngularPodcastTutorialApp'));

    var SearchformCtrl,
        scope,
        $state;

    // Initialize the controller and a mock scope
    beforeEach(inject(function ($controller, $rootScope, _$state_) {
        $state = _$state_;
        scope = $rootScope.$new();
        SearchformCtrl = $controller('SearchFormCtrl as searchFormCtrl', {
            $scope: scope,
            $state: $state
        });

    }));

    it('should change the route when submitting the values', function () {
        expect(scope.searchFormCtrl.submit).to.be.a('function');
        sinon.stub($state, 'go', function () {}); //Stub instead of spy to prevent us from actually attempting to change state
        scope.searchFormCtrl.submit('Angular Podcasts');
        expect($state.go).to.have.been.calledOnce.and.calledWithExactly('search', { term: 'Angular Podcasts' });
        $state.go.restore();
    });

});

And make it pass:

'use strict';

function SearchFormCtrl ($state) {

    this.submit = function (term) {
        $state.go('search', { term: term });
    };

}

/**
 * @ngdoc function
 * @name angularPodcastTutorialApp.controller:SearchformCtrl
 * @description
 * # SearchformCtrl
 * Controller of the angularPodcastTutorialApp
 */
angular.module('angularPodcastTutorialApp')
    .controller('SearchFormCtrl', SearchFormCtrl);

That one was easy!

Lets update the directive:


/**
 *
 * @returns {{scope: {}, templateUrl: string, controller: string, controllerAs: string, restrict: string}}
 */
function uvdSearchForm() {
    return {
        scope: {},
        templateUrl: 'views/directives/uvd-search-form.html',
        controller: 'SearchFormCtrl',
        controllerAs: 'searchFormCtrl',
        restrict: 'E'
    };
}

/**
 * @ngdoc directive
 * @name angularPodcastTutorialApp.directive:uvdSearchForm
 * @description
 * # uvdSearchForm
 */
angular.module('angularPodcastTutorialApp')
    .directive('uvdSearchForm', uvdSearchForm);

Add the uvd-search-form.html template:

<form role="search" name="search-form" ng-submit="searchFormCtrl.submit(term)">
    <div class="form-group">
        <input type="text" class="form-control" ng-model="term" placeholder="Search">
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
</form>

And add a header containing the directive to index.html

<nav class="navbar navbar-default" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">UVD Podcast Listener</a>
        </div>
        <uvd-search-form class="navbar-form navbar-left"></uvd-search-form>
    </div>
</nav>

Congratulations, you should now have a working podcast search tool! If something went wrong feel free to get in touch in the comments or skip and grab the next tagged version from github.

Finishing off the E2E tests

Great, we’ve now finished off the functionality for our feature. It’s time to finish off that E2E test that we started. A rule of thumb for automated tests is that they should never communicate with a 3rd party API. This will make sure that we can test our application even if:

  1. The iTunes API is unavailable
  2. We are offline
  3. We get banned for hammering the API with requests
  4. new results are added to iTunes, breaking our tests

It will also give us a speed benefit as the test should all be performing locally. We will be using a node tool called robohydra to act as a substitute for the Itunes API for this test.

Now create a json file named e2e-apis.json in the root of the project with the following content:

{
    "plugins": [
        { "name": "itunes-api" }
    ]
}

This will be where we define any mock apis that we will use in order to complete our E2E tests.

Create the following directory structure in the root of the application:

robohydra/
    plugins/
        itunes-api/

And add a JavaScript file named index.js to the itunes-api directory we’ve just created with the following content:

var RoboHydraHead = require("robohydra").heads.RoboHydraHead;

exports.getBodyParts = function (conf) {
    return {
        heads: [
            new RoboHydraHead({
                path: '/search',
                handler: function (req, res) {

                    var response,
                        responseData;

                    if (req.queryParams.term === 'JavaScript') {

                        responseData = {
                            resultCount: 2,
                            results: [
                                {"wrapperType": "track", "kind": "podcast", "collectionId": 442109513, "trackId": 442109513, "artistName": "hi@javascriptshow.com (The Javascript Show)", "collectionName": "The Javascript Show", "trackName": "The Javascript Show", "collectionCensoredName": "The Javascript Show", "trackCensoredName": "The Javascript Show", "collectionViewUrl": "https://itunes.apple.com/us/podcast/the-javascript-show/id442109513?mt=2&uo=4", "feedUrl": "http://feeds.feedburner.com/the-javascript-show", "trackViewUrl": "https://itunes.apple.com/us/podcast/the-javascript-show/id442109513?mt=2&uo=4", "artworkUrl30": "http://a4.mzstatic.com/us/r30/Podcasts/6a/ba/86/ps.kgrwrfsj.30x30-50.jpg", "artworkUrl60": "http://a3.mzstatic.com/us/r30/Podcasts/6a/ba/86/ps.kgrwrfsj.60x60-50.jpg", "artworkUrl100": "http://a2.mzstatic.com/us/r30/Podcasts/6a/ba/86/ps.kgrwrfsj.100x100-75.jpg", "collectionPrice": 0.00, "trackPrice": 0.00, "trackRentalPrice": 0, "collectionHdPrice": 0, "trackHdPrice": 0, "trackHdRentalPrice": 0, "releaseDate": "2013-03-01T23:16:00Z", "collectionExplicitness": "notExplicit", "trackExplicitness": "notExplicit", "trackCount": 8, "country": "USA", "currency": "USD", "primaryGenreName": "Tech News", "radioStationUrl": "https://itunes.apple.com/station/idra.442109513", "artworkUrl600": "http://a3.mzstatic.com/us/r30/Podcasts/6a/ba/86/ps.kgrwrfsj.600x600-75.jpg", "genreIds": ["1448", "26", "1318"], "genres": ["Tech News", "Podcasts", "Technology"]},
                                {"wrapperType": "track", "kind": "podcast", "artistId": 825701899, "collectionId": 496893300, "trackId": 496893300, "artistName": "DevChat.tv", "collectionName": "JavaScript Jabber", "trackName": "JavaScript Jabber", "collectionCensoredName": "JavaScript Jabber", "trackCensoredName": "JavaScript Jabber", "artistViewUrl": "https://itunes.apple.com/us/artist/devchat.tv/id825701899?mt=2&uo=4", "collectionViewUrl": "https://itunes.apple.com/us/podcast/javascript-jabber/id496893300?mt=2&uo=4", "feedUrl": "http://feeds.feedwrench.com/JavaScriptJabber.rss", "trackViewUrl": "https://itunes.apple.com/us/podcast/javascript-jabber/id496893300?mt=2&uo=4", "artworkUrl30": "http://a3.mzstatic.com/us/r30/Podcasts/v4/42/f8/13/42f813c0-0de4-0609-e2c5-9954f543eaf9/mza_3131735023717016958.30x30-50.jpg", "artworkUrl60": "http://a5.mzstatic.com/us/r30/Podcasts/v4/42/f8/13/42f813c0-0de4-0609-e2c5-9954f543eaf9/mza_3131735023717016958.60x60-50.jpg", "artworkUrl100": "http://a4.mzstatic.com/us/r30/Podcasts/v4/42/f8/13/42f813c0-0de4-0609-e2c5-9954f543eaf9/mza_3131735023717016958.100x100-75.jpg", "collectionPrice": 0.00, "trackPrice": 0.00, "trackRentalPrice": 0, "collectionHdPrice": 0, "trackHdPrice": 0, "trackHdRentalPrice": 0, "releaseDate": "2014-10-29T13:00:00Z", "collectionExplicitness": "notExplicit", "trackExplicitness": "notExplicit", "trackCount": 131, "country": "USA", "currency": "USD", "primaryGenreName": "Training", "radioStationUrl": "https://itunes.apple.com/station/idra.496893300", "artworkUrl600": "http://a2.mzstatic.com/us/r30/Podcasts/v4/42/f8/13/42f813c0-0de4-0609-e2c5-9954f543eaf9/mza_3131735023717016958.600x600-75.jpg", "genreIds": ["1470", "26", "1304", "1318", "1480"], "genres": ["Training", "Podcasts", "Education", "Technology", "Software How-To"]}
                            ]
                        }

                    } else {

                        responseData = {
                            resultCount: 0,
                            results: []
                        };

                    }

                    response = req.queryParams.callback + '(' + JSON.stringify(responseData) + ');';

                    res.send(response);
                }
            })
        ]
    };
};

note that it is not best practice to dump raw fixtures into robohydra heads, a much better idea would be to use some kind of fixture loading solution, but that is out of the scope of this tutorial.

If we needed more flexibility we could prime robohydra each time to tell it what to respond, but for our simple use-case we simply return two expected results if the term is ‘JavaScript’ and no results otherwise.

Okay so now we’ve set up robohydra we need to be able to specify the mock API for testing, and the real API for everything else. Thankfully there is a plugin for that! grunt-ng-constant allows to create angular parameters at run-time. We’ve already installed it, so we just have to add the following configuration:

    ngconstant: {
        options: {
            space: '  ',
            name: 'config',
            dest: '.tmp/scripts/config.js',
            wrap: true
        },
        serve: {
            constants: {
                ITUNES_BASE_URL: 'https://itunes.apple.com/'
            }
        },
        e2e: {
            constants: {
                ITUNES_BASE_URL: 'http://localhost:3000/'
            }
        }
    }

Add the task to your e2etest and serve sequences:

    grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
        if (target === 'dist') {
            return grunt.task.run(['build', 'connect:dist:keepalive']);
        }

        grunt.task.run([
            'clean:server',
            'wiredep',
            'concurrent:server',
            'autoprefixer:server',
            'ngtemplates',
            'ngconstant:serve',
            'connect:livereload',
            'watch'
        ]);
    });

    grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
        grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
        grunt.task.run(['serve:' + target]);
    });

    grunt.registerTask('e2e', [
        'clean:server',
        'ngconstant:e2e',
        'wiredep',
        'concurrent:server',
        'autoprefixer',
        'connect:e2e',
        'shell:protractor'
    ]);

    grunt.registerTask('test', [
        'clean:server',
        'wiredep',
        'ngconstant:serve',
        'concurrent:test',
        'autoprefixer',
        'ngtemplates',
        'connect:test',
        'karma'
    ]);

And add the new file to your index.html:

<script src="scripts/config.js"></script>

Add the new module config to your app.js

'use strict';

/**
 * @ngdoc overview
 * @name angularPodcastTutorialApp
 * @description
 * # angularPodcastTutorialApp
 *
 * Main module of the application.
 */
angular
    .module('angularPodcastTutorialApp', [
        'ui.router',
        'restangular',
        'config'
    ]);

If you now restart grunt test or grunt serve you should see a new file in your .tmp directory called config.js, which creates a new angular module called ‘config’ and defines one constant called ITUNES_BASE_URL. This is the file that will change depending on if we are serving and application normally or running our E2E tests.

Set the ItunesApi to use the constant as the base url:

function ItunesApi(Restangular, ITUNES_BASE_URL) {

    /**
     * If search results, only return the results.
     *
     * @param data          the response data
     * @param operation
     * @param what
     * @returns Array
     */
    function searchResponseInterceptor (data, operation, what) {

        if (operation === 'getList' && what === 'search') {
            return data.results;
        }

        return data;
    }

    return Restangular.withConfig(function (RestangularConfigurer) {
        RestangularConfigurer.setBaseUrl(ITUNES_BASE_URL);
        RestangularConfigurer.setJsonp(true);
        RestangularConfigurer.setDefaultRequestParams('jsonp', { callback: 'JSON_CALLBACK', media: 'podcast' });
        RestangularConfigurer.setResponseInterceptor(searchResponseInterceptor);
    });

}

For E2E tests I strongly advise using the Page Object pattern. This seperates the steps of your test from the actual UI of the application. For example, we need to interact with the search form in some of our steps. Rather than directly finding the input in the DOM, we’re going to construct a page object which represents the search form, and construct an API that we can interact with in our step definitions. Create the search-page.js page object under features/page_object directory:

var SearchPage = {

    form: $('input[ng-model="term"]'),

    searchResults: by.repeater('podcast in searchCtrl.results'),

    errorMessage: $('#search-error-message'),

    visit: function () {
        return browser.get('#/search');
    },

    search: function (term) {
        return this.form.sendKeys(term + protractor.Key.ENTER);
    },

    getSearchResults: function () {
        return getRepeaterColumns(this.searchResults, ['collectionName', 'feedUrl']);
    },

    countSearchResults: function () {
        return getRepeaterCount(this.searchResults);
    },

    getAlertText: function () {
        return this.errorMessage.getText();
    },

    hasAlert: function () {
        return this.errorMessage.isPresent();
    }
};

module.exports = SearchPage;

As you can see, our page object has a nice user-friendly API that we can interact with and we can base the assertions of the steps on the results of these. If any of this looks completely alien to you I suggest reading up on protractor docs. You may have noticed the getRepeaterColumns andgetRepeaterCount functions in the global namespace. I’ve created these two functions as helpers in world.js. They iterate over an angular repeater and get the columns specified, in this instance they are looking for our collectionName and feedUrl fields and returning them in a nice table, with the structure we have used in our test:

Title Feed
The Javascript Show http://feeds.feedburner.com/the-javascript-show
JavaScript Jabber http://feeds.feedwrench.com/JavaScriptJabber.rss

We’ll need to install a few more node modules, in your terminal type npm install --save-dev Q and npm install --save-dev lodash and npm install --save-dev chai-as-promisedQ is a promise library, a watered down version is actually included in angular under the $q name.Lodash is a functionality utility library and chai-as-promised will save us time making assertions on promises. Ive set these libraries to be available globally with node’s global keyword.

Finally, update our step definitions in user-searches-for-a-podcast.js to complete our E2E test:

function UserSearchesForAPodcast() {

    'use strict';
    this.World = require('../support/world.js').World;
    var searchPage = require('../page_object/search-page');

    this.Given(/^that I am on the podcast search page$/, function (callback) {
        searchPage.visit().then(callback);
    });

    this.When(/^I search for the podcast "([^"]*)"$/, function (term, callback) {
        searchPage.search(term).then(callback);
    });

    this.Then(/^I should see the following search results:$/, function (table, callback) {
        expect(searchPage.getSearchResults()).to.eventually.deep.equal(table.rows()).notify(callback);
    });

    this.Then(/^I should see no search results$/, function (callback) {
        expect(searchPage.countSearchResults()).to.eventually.equal(0).notify(callback);
    });

    this.Then(/^I should see the alert "([^"]*)"$/, function (message, callback) {
        expect(searchPage.getAlertText()).to.eventually.equal(message).notify(callback);
    });

    this.Then(/^I should not see an alert$/, function (callback) {
        expect(searchPage.hasAlert()).to.eventually.be.false.notify(callback);
    });
}

module.exports = UserSearchesForAPodcast;

You’ll notice the use of the eventually word in our chai assertions, these tell chai to wait until the promise has been resolved before checking expectations. You should also notice the use of notify afterwards, which is used to make sure we only call the callback to go to the next step when the promise has been resolved. This stops protractor falling over itself (and failing after it appears to have moved on to the next step). Install chai-as-promised and save it to our package.json file now with ‘npm install chai-as-promised –save-dev’.

If you run robohydra now with robohydra e2e-apis.json in one terminal to start the mock api and then run the e2e tests with grunt e2e in another you should see the following in lovely green output in your terminal:

Note that due to some changes in how angular/protractor deals with bindings, currently the steps in World.js only work with angular 1.2. I plan to update this for 1.3 soon but for now, if you want these E2E tests to work, you’ll have to stick with the older version.


Feature: User searches for a podcast

As a user of the podcast application
I want to search for podcasts that I know
In order to be able to find episodes to listen to

Scenario: User finds positive results for their search query # features/user-searches-for-a-podcast.feature:10
Given that I am on the podcast search page # features/user-searches-for-a-podcast.feature:8
When I search for the podcast "JavaScript" # features/user-searches-for-a-podcast.feature:11
Then I should see the following search results: # features/user-searches-for-a-podcast.feature:12
| Title | Feed |
| The Javascript Show | http://feeds.feedburner.com/the-javascript-show |
| JavaScript Jabber | http://feeds.feedwrench.com/JavaScriptJabber.rss |
And I should not see an alert # features/user-searches-for-a-podcast.feature:16

Scenario: User finds no results for their podcast query # features/user-searches-for-a-podcast.feature:18
Given that I am on the podcast search page # features/user-searches-for-a-podcast.feature:8
When I search for the podcast "07yfabOAgiuae" # features/user-searches-for-a-podcast.feature:19
Then I should see no search results # features/user-searches-for-a-podcast.feature:20
And I should see the alert "No search results" # features/user-searches-for-a-podcast.feature:21

2 scenarios (2 passed)
8 steps (8 passed)
Shutting down selenium standalone server.

Done, without errors.

Optional improvement

So now we have a fully working E2E suite for our first feature that we can run by starting robohydra and then running a grunt task. Thats great, but why are we doing two things to run our tests? We have a task runner in Grunt and we’re typing in an extra 22 chars each time we want to run what is essentially another node task? Why are we putting ourselves through this immense pain!?

Let’s automate that robohydra task with grunt-shell. Update the shell task in your Gruntfile with the robohydra task:

    shell: {
        robohydra: {
            command: 'robohydra e2e-apis.json',
            options: {
                async: true
            }
        },
        protractor: {
            command: './node_modules/protractor/bin/protractor protractor-conf.js'
        }
    },

Because robohydra must be run in the background we’ve set the async flag. Now update our e2e sequence:

grunt.registerTask('e2e', [
    'clean:server',
    'ngconstant:e2e',
    'wiredep',
    'concurrent:server',
    'autoprefixer',
    'connect:e2e',
    'shell:robohydra',
    'shell:protractor',
    'shell:robohydra:kill',
]);

Now if we run grunt e2e the task will start and stop robohydra automatically for us!

Congratulations. We’re really done this time (promise).

Continue to part 3.

Share: