Chapter 13. Testing with Mocha, Karma, and More

Testing is an integral part of software development, especially when dealing with applications that interact with end-users and various clients, as is the case with JavaScript SPAs. The results of web application code can often be unpredictable due to the variety of clients potentially consuming the application, so all possible scenarios should be accounted for and tested appropriately.

In this chapter, we will cover the following topics:

  • What is unit testing, integration testing, and end-to-end (E2E) testing?
  • How to perform JavaScript unit testing with Mocha, Chai, and Sinon.js
  • How to configure Karma with Jasmine to test AngularJS
  • How to perform unit testing with AngularJS
  • How to perform end-to-end testing with AngularJS

Types of testing

There are various types of testing known throughout the software industry, but there are three main types that are consistently used, especially in web application development. They are as follows:

  • Unit testing
  • Integration testing
  • End-to-end testing, also known as functional testing

These three types of testing comprise what is known as the software testing pyramid. The pyramid can be broken down into more granular forms of testing, but this is how it looks from a high vantage point:

Types of testing

Unit testing

The bottom level of the software testing pyramid is unit testing. Unit testing targets the smallest pieces of an application, or units, in isolation from the remainder of the application. A unit is typically an individual method or object instance. When you test a unit in isolation, it means that the test should not interact with any application dependencies, such as network access, database access, user sessions, and any other dependencies that may be needed in the real-world application context. Instead, a unit test should only perform operations within local memory.

The goal of any unit test should be to test only a single feature of the application, and that feature should be encapsulated within the unit. If that unit does have any dependencies, they should be mocked, or simulated, instead of invoking the actual dependencies. We will discuss more about that later in the chapter.

Knowing that you will perform unit testing will help you write smaller and more focused methods in your application because they are easier to test. Many will argue that you should always write your tests first before writing any application code. This is not always practical, however, because you may have been pushed into a speedy development cycle that didn't allow time for the tedious process of writing unit tests. Writing unit tests against existing code may prove tedious as well, but it is quite acceptable, and better than having no unit tests at all.

Let's look at some well-known JavaScript unit testing frameworks that can quickly and easily be integrated into a new or existing application.

Mocha

Mocha is a popular JavaScript unit testing framework that is commonly used throughout the Node.js community. Let's revisit our Node.js sample project from the beginning of the book and install Mocha so we can try out a few unit testing examples:

$ npm install mocha -g

Install mocha globally so that you can access it easily from any directory.

Now, let's create a test directory at the root of our project to store testing related files:

$ mkdir test

Create a file in the test directory named test.js and open it for editing. Place the following code in the file and save it:

var assert = require('assert'); 
describe('String', function() { 
    describe('#search()', function() { 
        it('should return -1 when the value is not present', function() { 
            assert.equal(-1, 'text'.search(/testing/)); 
        }); 
    }); 
}); 

To run the test, issue the following command from your console under the test directory:

$ mocha test.js

You should then see the following output in your console:

 String
    #search()
       should return -1 when the value is not present
  1 passing (8ms)

Using the Mocha describe method, this unit test performs a simple assertion on the search method of the String constructor. An assertion in testing is simply an evaluation of whether something is true or not. In this example, we are testing the assertion that the search method returns -1 when its argument is not found within the search context.

Assertions with Chai

The previous example uses the Node.js assert module, but with Mocha, you will want to use a full-fledged assertion library for a substantive testing environment. Mocha is compatible with multiple JavaScript assertion libraries, including the following:

  • Should.js
  • Expect.js
  • Chai
  • Better-assert
  • Unexpected

Chai is a popular and active open source assertion library, so we will use it for our Mocha assertion examples throughout this chapter. First, install chai in your local Node.js environment:

$ npm install chai --save-dev

Chai includes three styles of assertions, should, expect, and assert, allowing you to choose the flavor you like best.

Should-style assertions

Should-style assertions are accessible in Chai using chai.should(). This interface allows for a chainable method syntax that is familiar to many JavaScript developers, especially if you have worked with libraries such as jQuery. The chainable method names use natural language to make writing the tests more fluid. Additionally, Chai's should method extends Object.prototype so that you can chain it directly to the variables you are testing, as follows:

var should = require('chai').should(); // Execute the should function 
var test = 'a string'; 
test.should.be.a('string'); 

This example will perform a simple assertion that the given variable is a string.

Expect-style assertions

Expect-style assertions are accessible in Chai using chai.expect. This interface is similar to should, in that, it uses method chaining, but it does not extend Object.prototype, so it is used in a more traditional fashion, as follows:

var expect = require('chai').expect; 
var test = 'a string'; 
expect(test).to.be.a('string); 

This example performs the same assertion as the previous example, but with Chai's expect method instead of should. Notice that the require call to the expect method does not execute it, as is the case with should.

Assert-style assertions

Assert-style assertions are accessible in Chai using chai.assert. This interface uses the more traditional style of assertions, much like the Node.js native assert module:

var assert = require('chai').assert; 
var test = 'a string'; 
assert.typeOf(test, 'string'); 

This example performs the same assertion as the two previous examples but with Chai's assert method. Notice that this example calls upon the assert.typeOf method, which is akin to the native JavaScript typeof operator, rather than using natural language method names as should and expect do.

Testing with Mocha and Chai does not favor any particular style of assertion available in Chai, but it is best to choose one and stick with it so that a testing pattern is established. We will use the should style of assertion for the remaining examples in this chapter.

Using Mocha with Chai-and Should-style assertions

Now, let's go back to our original Mocha test example in test.js and add a similar test right under it, but use Chai's should assertion method instead:

var should = require('chai').should(); 
describe('String', function() { 
    describe('#search()', function() { 
        it('should return -1 when the value is not present', function() { 
            'text'.search(/testing/).should.equal(-1); 
        }); 
    }); 
}); 

This performs the same test as shown earlier using the native Node.js assert module, but with Chai's should method instead. The advantage of working with Chai in this scenario, however, is that it provides additional tests beyond what Node.js can provide out of the box, and the Chai tests are also browser compatible.

Back in the console, run the Mochas tests:

$ mocha test.js

This should yield the following output from your two tests:

  String
    #search()
       should return -1 when the value is not present
  String
    #search()
       should return -1 when the value is not present
  2 passing (9ms)

Now, let's write a more interesting test that might be used in a real-world application context. A JavaScript SPA will often be dealing with the DOM, so we should test that interaction accordingly. Let's consider the following method as an example:

module.exports = { 
    addClass: function(elem, newClass) { 
        if (elem.className.indexOf(newClass) !== -1) { 
            return; 
        } 
        if (elem.className !== '') { 
            newClass = ' ' + newClass; 
        } 
        elem.className += newClass; 
    } 
}; 

The addClass method simply adds a className to a DOM element if it does not already have that className. We are defining it with module.exports so that it is consumable as a Node.js module. To test this code, save it in a new file named addClass.js under your test directory.

Now, back in the test.js file, add the following unit test code under the other two tests that we have written so far:

var addClass = require('./addClass').addClass; 
describe('addClass', function() { 
    it('should add a new className if it does not exist', function() { 
        var elem = { 
            className: 'existing-class' 
        }; 
        addClass(elem, 'new-class'); 
        elem.className.split(' ')[1].should.equal('new-class'); 
    }); 
}); 

Due to the no-dependencies constraint of unit testing, we are faking, or mocking, a DOM element here by defining a simple JavaScript object called elem and giving it a className property, just as a real DOM object would have. This test is written strictly to assert that calling addClass on an element with a new, non-existent className will, in fact, add that className to the element.

Running the tests from the command line should now yield the following output:

  String
    #search()
       should return -1 when the value is not present
  String
    #search()
       should return -1 when the value is not present
  addClass
     should add a new className if it does not exist
  3 passing (10ms)

Running Mocha tests in the browser

Mocha is easy enough to run from a CLI, but it also comes bundled with assets that allow you to easily run your tests in a browser. As we are currently working with frontend JavaScript code, it is best to test it in the environment it will actually be run. To do this, let's first create a file named test.html at the root of the project and add the following markup to it:

<!doctype html> 
<html> 
    <head> 
        <title>Mocha Tests</title> 
        <link rel="stylesheet" href="node_modules/mocha/mocha.css"> 
    </head> 
    <body> 
        <div id="mocha"></div> 
        <script src="node_modules/mocha/mocha.js"></script> 
        <script src="node_modules/chai/chai.js"></script> 
        <script>mocha.setup('bdd');</script> 
        <script src="test/addClass.js"></script> 
        <script src="test/test.js"></script> 
        <script> 
            mocha.run(); 
        </script> 
    </body> 
</html> 

Mocha provides CSS and JavaScript assets to view tests in a browser. All that is required of the DOM structure is to have a <div> defined with an ID of mocha. The styles should be included in <head>, and the JavaScript should be included under <div id="mocha">. Additionally, the call to mocha.setup('bdd') tells the Mocha framework to use its Behavior-Driven Development(BDD) interface for testing.

Now, remember that our JavaScript files are written as Node.js modules, so we will have to modify their syntax to work properly in a browser context. For our addClass.js file, let's modify the method to be defined in a global window object named DOM:

window.DOM = { 
    addClass: function(elem, newClass) { 
        if (elem.className.indexOf(newClass) !== -1) { 
            return; 
        } 
        if (elem.className !== '') { 
            newClass = ' ' + newClass; 
        } 
        elem.className += newClass; 
    } 
}; 

Next, modify test.js to load chai.should and DOM.addClass from the window context, instead of as Node.js modules, and let's go ahead and remove the original Node.js assert module test that we created:

// Chai.should assertion 
var should = chai.should(); 
describe('String', function() { 
    describe('#search()', function() { 
        it('should return -1 when the value is not present', function() { 
            'text'.search(/testing/).should.equal(-1); 
        }); 
    }); 
}); 
 
// Test the addClass method 
var addClass = DOM.addClass; 
describe('addClass', function() { 
    it('should add a new className if it does not exist', function() { 
        var elem = { 
            className: 'existing-class' 
        }; 
        addClass(elem, 'new-class'); 
        elem.className.split(' ')[1].should.equal('new-class'); 
    }); 
}); 

You should now have two tests contained in test.js. Finally, run a local Node.js server from the root of the project so that you can view the test.html page in a browser:

$ http-server

Using the global http-server module, your local server will be accessible to your browser at localhost:8080 and the test file at localhost:8080/test.html. Go to that page in a browser and you will see the tests run automatically. If everything is set up correctly, you should see the following output:

Running Mocha tests in the browser

Sinon.js

Due to the requirement of isolation in unit testing, dependencies must often be simulated by providing spies, stubs, and mocks or objects that imitate the behavior of real objects. Sinon.js is a popular JavaScript library that provides these tools for testing and it is compatible with any unit testing framework, including Mocha.

Spies

Test spies are functions that can be used in place of callback dependencies and also are used to spy or record arguments, return values, and any other related data to other functions that is used throughout an application. Spies are available in Sinon.js through the sinon.spy() API. It can be used to create an anonymous function that records data on itself every time it is called throughout a test sequence:

var spy = sinon.spy(); 

An example use case of this is testing that a callback function is invoked properly from another function in a publish and subscribe design pattern, as follows:

it('should invoke the callback on publish', function() { 
    var spy = sinon.spy(); 
    Payload.subscribe('test-event', spy); 
    Payload.publish('test-event'); 
    spy.called.should.equal(true); 
}); 

In this example, a spy is used to act as a callback for a Payload.js custom event. The callback is registered through the Payload.subscribe method and expected to be invoked upon publishing the custom event test-event. The sinon.spy() function will return an object with several properties available on it that give you information about the returned function. In this case, we are testing for the spy.called property, which will be true if the function was called at least once.

The sinon.spy() function can also be used to wrap another function and spy on it, as follows:

var spy = sinon.spy(testFunc); 

Additionally, sinon.spy() can be used to replace an existing method on an object and behave exactly like the original method, but with the added benefit of collecting data on that method through the API, as follows:

var spy = sinon.spy(object, 'method'); 

Stubs

Test stubs build on top of spies. They are functions that are spies themselves with access to the full test spy API, but with added methods to alter their behavior. Stubs are most often used when you want to force certain things to happen inside of functions when a test is being run on it, and also when you want to prevent certain things from happening.

For example, say that we have a userRegister function that registers a new user to a database. This function has a callback that is returned when a user is successfully registered, but if saving the user fails, it should return an error in that callback, as follows:

it('should pass the error into the callback if save fails', function() { 
    var error = new Error('this is an error'); 
    var save = sinon.stub().throws(error); 
    var spy = sinon.spy(); 
 
    registerUser({ name: 'Peebo' }, spy); 
 
    save.restore(); 
    sinon.assert.calledWith(spy, error); 
}); 

First, we will create an Error object to pass to our callback. Then, we will create a stub for our actual save method that replaces it and throws an error, passing the Error object to the callback. This replaces any actual database functionality as we cannot rely on real dependencies for unit testing. Finally, we will define the callback function as a spy. When we call the registerUser method for our test, we will pass the spy to it as its callback. In a scenario where we have a real save method, save.restore() will change it back to its original state and remove the stubbed behavior.

Sinon.js also has its own assertion library built in for added functionality when working with spies and stubs. In this case, we will call sinon.assert.calledWith() to assert that the spy was called with the expected error.

Mocks

Mocks in Sinon.js build upon both spies and stubs. They are fake methods, like spies, with the ability to add additional behaviors, like stubs, but also give you the ability to define expectations for the test before it is actually run.

Tip

Mocks should only be used once per unit test. If you find yourself using more than one mock in a unit test, you are probably not using them as intended.

To demonstrate the use of a mock, let's consider an example using the Payload.js localStorage API methods. We can define a method called incrementDataByOne that is used to increment a localStorage value from 0 to 1:

describe('incrementDataByOne', function() { 
    it('should increment stored value by one', function() { 
        var mock = sinon.mock(Payload.storage); 
        mock.expects('get').withArgs('data').returns(0); 
        mock.expects('set').once().withArgs('data', 1); 
 
        incrementDataByOne(); 
 
        mock.restore(); 
        mock.verify(); 
    }); 
}); 

Notice that instead of defining a spy or a stub here, we will define a mock variable that takes the Payload.storage object API as its only argument. A mock is then created on the object to test its methods for expectations. In this case, we will set up our expectations that the initial value of data should return 0 from the Payload.storage.get API method, and then after calling Payload.storage.set with 1, it should be incremented by 1 from its original value.

Jasmine

Jasmine is another popular unit testing framework in the Node.js community, and it is also used for most AngularJS applications and referenced throughout the AngularJS core documentation. Jasmine is similar to Mocha in many ways, but it includes its own assertion library. Jasmine uses expect style assertions, much like the Chai expect style assertions, which were covered earlier:

describe('sorting the list of users', function() { 
    it('sorts in ascending order by default', function() { 
        var users = ['Kerri', 'Jeff', 'Brenda']; 
        var sorted = sortUsers(users); 
        expect(sorted).toEqual(['Brenda', 'Jeff', 'Kerri']); 
    }); 
}); 

As you can see in this example, Jasmine uses describe and it method calls for its tests that are identical to those used in Mocha, so switching from one framework to the other is pretty straightforward. Having knowledge of both Mocha and Jasmine is quite useful as they are both used commonly throughout the JavaScript community.

Karma test runner

Karma is a JavaScript test runner that allows you to run your tests in browsers automatically. We have already demonstrated how to run Mocha unit tests in the browser manually, but when using a test runner such as Karma, this process is much easier to set up and work with.

Testing with Karma, Mocha, and Chai

Karma can be used with multiple unit testing frameworks, including Mocha. First, let's install the Node.js modules that we'll need to work with Karma, Mocha, and Chai:

$ npm install karma karma-mocha karma-chai --save-dev

This will install Karma and its Node.js plugins for Mocha and Chai to your local development environment and save them in your package.json file. Now, in order to have Karma launch tests in browsers on your system, we'll need to install plugins for those as well, which are as follows:

$ npm install karma-chrome-launcher karma-firefox-launcher --save-dev

This will install the launcher modules for the Chrome and Firefox browsers. If you do not have one or both of these browsers on your system, then install the launchers for one or two that you do have. There are Karma launcher plugins for all major browsers.

Next, we will need to create a config file for Karma to run our tests and launch the appropriate browsers. Create a file at the root of the project named karma.conf.js and add the following code to it:

module.exports = function(config) { 
    'use strict'; 
    config.set({ 
        frameworks: ['mocha', 'chai'], 
        files: ['test/*.js'], 
        browsers: ['Chrome', 'Firefox'], 
        singleRun: true 
    }); 
}; 

This configuration simply tells Karma that we're using the Mocha and Chai testing frameworks, we want to load all JavaScript files under the test directory, and we want to launch the tests to run in the Chrome and Firefox browsers, or the browsers that you have chosen. The singleRun parameter tells Karma to run the tests and then exit, rather than continue to run.

Now, all we have to do is run Karma from the CLI to run our tests in the defined browsers. As Karma is installed locally, you will have to add the relative path from your project root to the module in order to run it, as follows:

$ ./node_modules/karma/bin/karma start karma.conf.js

You will notice that this command also specifies the configuration file you want to use for your Karma instance, but it will default to the karma.conf.js file that you created at the root directory if you exclude it in the command.

Alternatively, if you would like to run Karma from any directory globally, you can install the karma-cli module, just like you did with Grunt and grunt-cli in Chapter 1 , Getting Organized with NPM, Bower, and Grunt:

$ npm install karma-cli -g

Tip

Make sure that you add the -g parameter so that karma is available as a global Node.js module.

Now, you can simply run the following command from the CLI:

$ karma start

Running this command will open the specified browsers automatically while yielding an output similar to the following command:

28 08 2016 18:02:34.147:INFO [karma]: Karma v1.2.0 server started at
    http://localhost:9876/
28 08 2016 18:02:34.147:INFO [launcher]: Launching browsers Chrome, Firefox
    with unlimited concurrency
28 08 2016 18:02:34.157:INFO [launcher]: Starting browser Chrome
28 08 2016 18:02:34.163:INFO [launcher]: Starting browser Firefox
28 08 2016 18:02:35.301:INFO [Chrome 52.0.2743 (Mac OS X 10.11.6)]:
    Connected on socket /#TJZjs4nvaN-kNp3QAAAA with id 18074196
28 08 2016 18:02:36.761:INFO [Firefox 48.0.0 (Mac OS X 10.11.0)]:
    Connected on socket /#74pJ5Vl1sLPwySk4AAAB with id 24041937
Chrome 52.0.2743 (Mac OS X 10.11.6):
    Executed 2 of 2 SUCCESS (0.008 secs / 0.001 secs)
Firefox 48.0.0 (Mac OS X 10.11.0):
    Executed 2 of 2 SUCCESS (0.002 secs / 0.002 secs)
TOTAL: 4 SUCCESS

If you follow along from the beginning of this output, you can see that Karma launches its own server on port 9876 and then launches the specified browsers once it is running. Your two tests are run in each browser with success, thus a total of 4 SUCCESS is noted in the final line of the output.

The reason for doing this type of testing is so that your unit tests can run in multiple browsers and you can ensure that they pass in all of them. With frontend JavaScript, there is always the possibility that one browser will work differently than another, so as many scenarios as possible should be tested so you can be sure that your app won't have bugs in some browsers that may be experienced by any end users with those browsers.

This is also a great way to help you define the browsers you want to support for your application and which browsers you may want to detect and notify the user that it is not supported. This is a common practice when you want to use modern JavaScript techniques and methods that may not be supported by older, outdated browsers.

Testing AngularJS with Karma and Jasmine

The AngularJS community has embraced Jasmine as its unit testing framework of choice, and it can also be used with Karma. Let's install our dependencies to work with Karma and Jasmine now:

$ npm install jasmine karma-jasmine --save-dev

This will install the Jasmine unit testing framework and its corresponding plugin for Karma, saving it to your package.json file.

Now, let's install AngularJS to our sample project, simply to test example code, so we can learn how to apply unit testing to our actual AngularJS app.

AngularJS is available on both NPM and Bower. We will use Bower for the following example, as this is for frontend code:

$ bower install angular --save

Save angular as a dependency. Next, install the angular-mocks library as a development dependency:

$ bower install angular-mocks --save-dev

The angular-mocks library gives you the ngMock module, which can be used in your AngularJS applications to mock services. Additionally, you can use it to extend other modules and make them behave synchronously, providing for more straightforward testing.

Now, let's change the karma.conf.js file to reflect the use of Jasmine instead of Mocha, and the addition of angular-mocks. Your configuration should look like the following code block:

module.exports = function(config) { 
    'use strict'; 
    config.set({ 
        frameworks: ['jasmine'], 
        files: [ 
            'bower_components/angular/angular.js', 
            'bower_components/angular-mocks/angular-mocks.js', 
            'test/angular-test.js' 
        ], 
        browsers: ['Chrome', 'Firefox'], 
        singleRun: true 
    }); 
}; 

Here, we have changed the frameworks parameter of the Karma configuration to use only Jasmine. Jasmine can be dropped in as a replacement for both Mocha and Chai because Jasmine includes its own assertion methods. Additionally, we have added angular.js and angular-mocks.js from the bower_components directory to our files array to test AngularJS code with ngMock. Under the test directory, we will load a new file named angular-test.js.

Now, let's use Jasmine and ngMock to write some tests for a simplified version of DashMainController, which we wrote for the gift app in Chapter 10 , Displaying Views. Create a file under the test directory named angular-test.js and add the following code:

var giftappControllers = angular.module('giftappControllers', []); 
angular.module('giftappControllers') 
    .controller('DashMainController', ['$scope', function($scope, List) { 
        $scope.lists = [ 
            {'name': 'Christmas List'}, 
            {'name': 'Birthday List'} 
        ]; 
    }]); 

This will load the giftappControllers module into memory and subsequently register DashMainController. We are excluding any other services and factories here to ensure the isolated testing of the controller. Next, let's write a simple Jasmine test to assert that the length of the $scope.lists array is 2:

describe('DashMainController', function() { 
    var $controller; 
    beforeEach(module('giftappControllers')); 
    beforeEach(inject(function(_$controller_) { 
        $controller = _$controller_; 
    })); 
    describe('$scope.lists', function() { 
        it('has a length of 2', function() { 
            var $scope = {}; 
            var testController = $controller('DashMainController', { 
                $scope: $scope 
            }); 
            expect($scope.lists.length).toEqual(2); 
        }); 
    }); 
}); 

In the initial describe call for DashMainController, we will initialize a $controller variable that will be used to represent the AngularJS $controller service. Additionally, we will make two calls to the Jasmine beforeEach method. This allows code to be run before each test is run and do any setup that is needed. In this case, we will need to initialize the giftappControllers module, done in the first call to beforeEach, and next we must assign the local $controller variable to the AngularJS $controller service.

In order to access the AngularJS $controller service, we will use the angular-mock inject method, which wraps a function into an injectable function, making use of Angular's dependency injector. This method also includes a convention in which you can place an underscore on each side of an argument name and it will get injected properly without conflicting with your local variable names. Here, we will do this with the _$controller_ argument, which is interpreted by the inject method as Angular's $controller service. This allows us to use the local $controller variable to take its place and keep the naming convention consistent.

With this code in place, you are ready to run the test, as follows:

$ karma start

This will yield an output similar to the following command:

03 09 2016 01:42:58.563:INFO [karma]: Karma v1.2.0 server started at
    http://localhost:9876/
03 09 2016 01:42:58.567:INFO [launcher]: Launching browsers Chrome, Firefox
    with unlimited concurrency
03 09 2016 01:42:58.574:INFO [launcher]: Starting browser Chrome
03 09 2016 01:42:58.580:INFO [launcher]: Starting browser Firefox
03 09 2016 01:42:59.657:INFO [Chrome 52.0.2743 (Mac OS X 10.11.6)]:
    Connected on socket /#sXw8Utn7qjVLwiqKAAAA with id 15753343
Chrome 52.0.2743 (Mac OS X 10.11.6):
    Executed 1 of 1 SUCCESS (0.038 secs / 0.03 secs)
Chrome 52.0.2743 (Mac OS X 10.11.6):
    Executed 1 of 1 SUCCESS (0.038 secs / 0.03 secs)
Firefox 48.0.0 (Mac OS X 10.11.0):
    Executed 1 of 1 SUCCESS (0.001 secs / 0.016 secs)
TOTAL: 2 SUCCESS

You should see that the test passed in all browsers because the length of the $scope.lists array is 2, as the Jasmine assertion tested for.

Integration testing

The second level of the software testing pyramid is integration testing. Integration testing involves testing at least two units of code that interact with each other, so in its simplest form, an integration test will test the outcome of two unit tests, such that they integrate with your application as expected.

The idea behind integration testing is to build upon your unit tests by testing larger pieces, or components, of your application. It is possible that all of your unit tests may pass because they are tested in isolation, but when you start testing the interaction of those units with each other, the outcome may not be what you expect. This is why unit testing alone is not sufficient to adequately test a SPA. Integration testing allows you to test key functionality in various components of your application before you move on to end-to-end testing.

End-to-end testing

The top level of the software testing pyramid is end-to-end testing, abbreviated as E2E, and also referred to as functional testing. The goal of end-to-end testing is to test the true functionality of your application's features in their entirety. For example, if you have a user registration feature in your app, an end-to-end test will ensure that the user is able to register properly through the UI, added to the database, a message to the user that they were successfully registered displayed, and, potentially, an e-mail sent to the user, or any other follow-up actions that may be required by your application.

The angular-seed project

In order to demonstrate a simple AngularJS application with examples of both unit and end-to-end testing, AngularJS created the angular-seed project. It is an open source project that is available on GitHub. Let's install it now so that we can run some simple unit and end-to-end testing with AngularJS.

Let's clone the angular-seed repository from GitHub into a new, clean project directory, as follows:

$ git clone https://github.com/angular/angular-seed.git
$ cd angular-seed

The angular-seed project has both NPM dependencies and Bower dependencies, but you only need to run the NPM install that will install the Bower dependencies for you:

$ npm install

This will install many tools and libraries, some of which you have seen already, including Jasmine, Karma, AngularJS, and angular-mocks. Next, all you have to do is start the NPM server using the following command line:

$ npm start

This will run a few tasks and then start up a Node.js server for you. You should see the following output:

> [email protected] prestart /angular-seed
> npm install
> [email protected] postinstall /angular-seed
> bower install
> [email protected] start /angular-seed
> http-server -a localhost -p 8000 -c-1 ./app
Starting up http-server, serving ./app
Available on:
  http://localhost:8000
Hit CTRL-C to stop the server

Now, go to http://localhost:8000 in a web browser and you will see a simple layout displayed. It consists of two view labels, view1 and view2, with view1 being displayed by default after the page loads. Each view requests a partial template file to be loaded upon the first view, and then caches it for any subsequent view.

Let's first run the angular-seed unit tests so we can see how they are set up. Karma is used to launch Jasmine unit tests, just as we did with our example controller test earlier; however, by default, they are set with the singleRun property in karma.conf.js set to false, which is intended for continuous integration. This allows Karma to watch for changes to your code as you make them so that the unit tests are run each time you save a file. In this way, you will get immediate feedback from the test runner and know if any tests are failing, which will prevent you from coding too far down a broken path.

To run the angular-seed tests in continuous integration mode, simply run the following NPM test command from the CLI:

$ npm test

This will yield an output similar to the following:

> [email protected] test /angular-seed
> karma start karma.conf.js
03 09 2016 13:02:57.418:WARN [karma]: No captured browser, open
    http://localhost:9876/
03 09 2016 13:02:57.431:INFO [karma]: Karma v0.13.22 server started at
    http://localhost:9876/
03 09 2016 13:02:57.447:INFO [launcher]: Starting browser Chrome
03 09 2016 13:02:58.549:INFO [Chrome 52.0.2743 (Mac OS X 10.11.6)]:
    Connected on socket /#A2XSbQWChmjkstjNAAAA with id 65182476
Chrome 52.0.2743 (Mac OS X 10.11.6):
    Executed 5 of 5 SUCCESS (0.078 secs / 0.069 secs)

This output shows that 5 of 5 unit tests were executed successfully. Notice that the command continues to run as it is in continuous integration mode. You will also have a Chrome browser window open that is awaiting file changes so that it can rerun the tests, the results of which will be immediately printed back to the CLI.

The project also includes a command to run Karma in singleRun mode, as we did with our previous Karma examples. To do this, hit Ctrl + C to close the currently running Karma instance. This will shut down the Chrome browser window as well.

Next, you will use the following NPM run command to launch Karma just once and shut back down:

$ npm run test-single-run

You will see the same output as you did earlier, but the browser window will open and close, the tests will run successfully, and the CLI will bring you back to the command prompt.

Now that we've done some simple unit testing with the angular-seed project, let's move on to end-to-end testing.

End-to-end testing with AngularJS and angular-seed

AngularJS emphasizes the importance of end-to-end testing and they have their own testing framework, Protractor,to do so. Protractor is an open source Node.js application that is built upon WebdriverJS, or just Webdriver, a component of the Selenium project.

Selenium has been around for a long time and is extremely well known throughout the web development community. It comprises multiple tools and libraries that allow for web browser automation. WebdriverJS is one of those libraries, and it is designed to test JavaScript applications.

Protractor is similar to Karma, in that, it is a test runner, but it designed to run end-to-end tests rather than unit tests. The end-to-end tests in the angular-seed project are written with Jasmine and Protractor is used to launch and run them.

First, we will need to install Webdriver as Protractor is built on top of it. The project comes with the following script to do this:

$ npm run update-webdriver

This will yield an output similar to the following, installing the latest version of Webdriver:

Updating selenium standalone to version 2.52.0
downloading https://selenium-release.storage.googleapis.com/2.52/selenium-
    server-standalone-2.52.0.jar...
Updating chromedriver to version 2.21
downloading 
    https://chromedriver.storage.googleapis.com/2.21/chromedriver_mac32.zip...
chromedriver_2.21mac32.zip downloaded to /angular-
    seed/node_modules/protractor/selenium/chromedriver_2.21mac32.zip
selenium-server-standalone-2.52.0.jar downloaded to /angular-
    seed/node_modules/protractor/selenium/selenium-server-standalone-2.52.0.jar

Once Webdriver is installed successfully, run the following NPM server again with Karma running so that Protractor can interact with the web application:

$ npm start

Next, as Protractor is set up to test with Chrome by default, we will need to bypass the Selenium server as it uses a Java NPAPI plugin that the newer versions of Chrome do not support. Fortunately, Protractor can test directly against both Chrome and Firefox, which circumvents this problem. To use a direct server connection with Chrome or Firefox, open the protractor.conf.js file in the E2E-tests directory, add a new configuration property named directConnect at the bottom, and set it to true. The Protractor config file should now look like the following block of code:

//jshint strict: false 
exports.config = { 
 
  allScriptsTimeout: 11000, 
 
  specs: [ 
    '*.js' 
  ], 
 
  capabilities: { 
    'browserName': 'chrome' 
  }, 
 
  baseUrl: 'http://localhost:8000/', 
 
  framework: 'jasmine', 
 
  jasmineNodeOpts: { 
    defaultTimeoutInterval: 30000 
  }, 
 
  directConnect: true 
 
}; 

Keep in mind that the directConnect setting is only intended to be used with Chrome and Firefox only. If you decide to run your tests in another browser, you will want to set it to false, or remove the property from the config, otherwise an error will be thrown. Using Chrome and Firefox to run your tests with directConnect also gives you a boost in speed when running your tests as the Selenium server is bypassed.

Now, with the server running, open another CLI session in the angular-seed root directory and run the following command for Protractor:

$ npm run protractor

The console output will indicate that ChromeDriver is being used directly and that one instance of WebDriver is running. You should see an output similar to the following command:

> [email protected] protractor /angular-seed
> protractor e2e-tests/protractor.conf.js
[14:04:58] I/direct - Using ChromeDriver directly...
[14:04:58] I/launcher - Running 1 instances of WebDriver
Started
...
3 specs, 0 failures
Finished in 1.174 seconds
[14:05:00] I/launcher - 0 instance(s) of WebDriver still running
[14:05:00] I/launcher - chrome #01 passed

Notice that 3 specs is indicated in the output? This indicates that those three E2E tests were run. Let's take a closer look at these tests by opening the e2e-tests/scenarios.js file in an editor.

At the beginning of this file, you will see an opening describe method call used to describe the application you are testing:

describe('my app', function() { 
    ... 
}); 

This describe block is used to contain all E2E tests for the application. Now, let's examine the first test:

it('should automatically redirect to /view1 when location hash/fragment is empty', function() { 
    browser.get('index.html'); 
    expect(browser.getLocationAbsUrl()).toMatch("/view1"); 
}); 

This test asserts that the application will redirect the URL in the browser to /#!/view1 when the #! route is empty. This is because the application is configured to auto-load the view111 partial when it loads, so the URL should reflect the route to that partial when it is loaded. You will notice that this does indeed occur when you load the application at http://localhost:8000 in your browser and it redirects to http://localhost:8000/#!/view1. This uses WebDriver's direct connection to Chrome to run the application and test the functionality through the browser API method, combined with an expect assertion that the URL matches the test path.

The second test in scenarios.js is a bit more verbose, as shown in the following block of code:

describe('view1', function() { 
 
    beforeEach(function() { 
        browser.get('index.html#!/view1'); 
    }); 
 
    it('should render view1 when user navigates to /view1', function() { 
        expect(element.all(by.css('[ng-view]
        p')).first().getText()).toMatch(/partial for view 1/); 
    }); 
 
}); 

This test asserts that the text shown in the view for the partial route /#!/view1 is in fact what it is expected to be. If you watch your developer console when you load the app in a browser, you will notice that it automatically makes an AJAX request to retrieve the local file, view1.html, which contains the partial for this view. The subsequent text that is displayed from this view is what this end-to-end test is looking for. This test uses the browser API method again, and additionally it uses the element API method to access DOM selectors, combined with an expect assertion that the text in the view matches the test string.

The third and final test in scenarios.js is much like the second test, but it is used to test the text shown in the view for the partial route rendered at /#!/view2. To view that text, first click on the view2 link in the running angular-seed application in your browser. You will see the URL update to view2, the console will show that another AJAX request is made to retrieve the local file view2.html, and the rendered view is updated, displaying the text (This is the partial for view 2). Now, let's take a look at the test, which is as follows:

describe('view2', function() { 
 
    beforeEach(function() { 
        browser.get('index.html#!/view2'); 
    }); 
 
    it('should render view2 when user navigates to /view2', function() { 
        expect(element.all(by.css('[ng-view] 
        p')).first().getText()).toMatch(/partial for view 2/); 
    }); 
 
}); 

For this test to work, the browser must first be directed to go to the /#!/view2 route so that the respective view will be displayed. This is accomplished by the beforeEach method that is run before the it method call. As discussed earlier, Jasmine provides the beforeEach method for any setup that needs to occur before each time a test is run. In this case, it runs code directing the browser API method to perform a get request to the /#!/view2 URL, which will subsequently update the view for the application to display the view2 partial. Only after this is complete will the test be run. This test also uses the element API method to access the DOM and find the text that it is looking to match against the expect assertion that the text (This is the partial for view 2) is found in the view.

End-to-end testing should certainly be more thorough for a real-world application, but the angular-seed project is a good place to start with experimenting on both unit testing and end-to-end testing for an AngularJS application. Once you have learned how it all works, gotten familiar with the Protractor and WebDriver APIs, and feel comfortable using Jasmine and Protractor together, you can begin writing custom tests for your own AngularJS applications with confidence.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.107.100