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:
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:
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:
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 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.
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:
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)
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:
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.
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');
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 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.
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 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 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.
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
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.
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.
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.
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.
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.
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.
3.146.34.146