End-to-end testing (or e2e for short) is on the other end of the spectrum as far as unit testing is concerned. The entire application exists as a black box, and the only controls at your disposal—for these tests—are actions the user might take inside the browser, such as firing click events or navigating to a page. Similarly, the correctness of tests is only verified by inspecting the state of the browser and the DOM itself.
More explicitly, an end-to-end test will (in some form) start up an actual instance of your application (or a subset of it), navigate to it in an actual browser, do stuff to a page, and look to see what happens on the page. It's pretty much as close as you are going to get to having an actual person sit down and use your application.
In this recipe, you'll put together a very basic end-to-end test suite so that you might better understand the concepts involved.
The code, links, and a live example related to this recipe are available at .
You'll begin with the code files created in the minimum viable application recipe from , Application Organization and Management. The most important files that you'll be editing here are AppComponent
and package.json
:
[package.json] { "scripts": { "start": "tsc && concurrently 'npm run tsc:w' 'npm run lite'", "lite": "lite-server", "postinstall": "npm install -s @types/node @types/core-js", "tsc": "tsc", "tsc:w": "tsc -w" }, "dependencies": { "@angular/common": "2.1.0", "@angular/compiler": "2.1.0", "@angular/core": "2.1.0", "@angular/platform-browser": "2.1.0", "@angular/platform-browser-dynamic": "2.1.0", "core-js": "^2.4.1", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.12", "systemjs": "0.19.27", "zone.js": "^0.6.23" }, "devDependencies": { "concurrently": "^2.2.0", "lite-server": "^2.2.2", "typescript": "^2.0.2" } } [app/app.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: '<h1>AppComponent template!</h1>' }) export class AppComponent {}
The Angular team maintains the Protractor project, which by many accounts is the best way to go about performing end-to-end tests on your applications, at least initially. It comes with a large number of utilities out of the box to manipulate the browser when writing your tests, and explicit integrations with Angular 2, so it's a terrific place to start.
Protractor relies on Selenium to automate the browser. The specifics of Selenium aren't especially important for the purpose of creating a minimum viable e2e test suite, but you will need to install a Java runtime:
sudo apt-get install openjdk-8-jre
I run Ubuntu, so the OpenJDK Java Runtime Environment V8 is suitable for my purposes. Your development setup may differ. Runtimes for different operating systems can be found on Oracle's website.
Protractor itself can be installed from npm, but it should be global. You'll be using it with Jasmine, so install it and its TypeScript typings as well:
npm install jasmine-core @types/jasmine --save-dev npm install protractor -g
You may need to fiddle with this configuration. Sometimes, it may work if you install protractor
locally rather than globally. Errors involving webdriver-manager
are part of the protractor
package, so they will most likely be involved where your protractor
package installation is as well.
It should come as no surprise that protractor is configured with a file, so create it now:
[protractor.conf.js] exports.config = { specs: [ './e2e/**/*.e2e-spec.ts' ], capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:3000/', framework: 'jasmine', }
None of these settings should surprise you:
e2e/
directory and will be suffixed with .e2e-spec.ts
localhost:3000
, and all the URLs inside the Protractor tests will be relative to thisFor simplicity, the server you are starting up for the end-to-end tests will be the same lite-server you've been using all along. When it starts up, lite-server will open up a browser window of its own, which will prove to be a bit annoying here. Since it is a thin wrapper for BrowserSync, you can configure it to not do this by simply directing it not to do so in a config file that is only used when running e2e tests.
Create this file now inside the test directory:
[e2e/bs-config.json] { "open": false }
The lite-server wrapper won't find this automatically, but you'll direct it to the file in a moment.
First, create a tsconfig.json
file inside the test directory:
[e2e/tsconfig.json] { "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "node" } }
Next, create the actual e2e test file skeleton:
[e2e/app.e2e-spec.ts] describe('App E2E Test Suite', () => { it('should have the correct h1 text', () => { }); });
This uses the standard Jasmine syntax to declare a spec suite and an empty test within it.
Before fleshing out the test, you need to ensure that Protractor can actually use this file. Install the ts-node plugin so that Protractor can perform the compilation and use these files in e2e tests:
npm install ts-node --save-dev
Next, instruct Protractor to use this to compile the test source files into a usable format. This can be done in its config file:
[protractor.conf.js] exports.config = { specs: [ './e2e/**/*.e2e-spec.ts' ], capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:3000/', framework: 'jasmine', beforeLaunch: function() { require('ts-node').register({ project: 'e2e' }); } }
With all this, you're left with a working but empty end-to-end test.
An excellent convention that I highly recommend using is the page object.
The idea behind this is that all of the logic surrounding the interaction with the page can be extracted into its own page object class, and the actual test behavior can use this abstracted page object inside the class. This allows the tests to be written independently of the DOM structure or routing definitions, which makes for superior test maintenance. What's more, it makes your tests totally independent of Protractor, which makes it easier should you want to change your end-to-end test runner.
For this simple end-to-end test, you'll want to specify how to arrive at this page and how to inspect it to get what you want. Define the page object as follows with two member methods:
[e2e/app.po.ts] import {browser, element, by} from 'protractor'; export class AppPage { navigate() { browser.get('/'); } getHeaderText() { return element(by.css('app-root h1')).getText(); } }
navigate()
instructs Selenium to the root path (which, as you may recall, is based on localhost:3000), and getHeaderText()
inspects a DOM element for its text contents.
Note that browser
, element
, and by
are all utilities imported from the protractor module. More on this later in the recipe.
With all of the infrastructure in place, you can now easily write your end-to-end test. You'll want to instantiate a new page object for each test:
[e2e/app.e2e-spec.ts] import {AppPage} from './app.po'; describe('App E2E Test Suite', () => { let page:AppPage; beforeEach(() => { page = new AppPage(); }); it('should have the correct h1 text', () => { page.navigate(); expect(page.getHeaderText()) .toEqual('AppComponent template!'); }); });
Finally, you'll want to give yourself the ability to easily run the end-to-end test suite. Selenium is often being updated, so it behoves you to explicitly update it before you run the tests:
[package.json] { "scripts": { "pree2e": "webdriver-manager update && tsc", "e2e": "concurrently 'npm run lite -- -c=e2e/bs-config.json' 'protractor protractor.conf.js'", "start": "tsc && concurrently 'npm run tsc:w' 'npm run lite'", "lite": "lite-server", "postinstall": "npm install -s @types/node @types/core-js", "tsc": "tsc", "tsc:w": "tsc -w" }, "dependencies": { ... }, "devDependencies": { ... } }
Finally, Angular 2 needs to integrate with Protractor and be able to tell it when the page is ready to be interacted with. This requires one more addition to the Protractor configuration:
[protractor.conf.js] exports.config = { specs: [ './e2e/**/*.e2e-spec.ts' ], capabilities: { 'browserName': 'chrome' }, baseUrl: 'http://localhost:3000/', framework: 'jasmine', useAllAngular2AppRoots: true, beforeLaunch: function() { require('ts-node').register({ project: 'e2e' }); } }
That's all! You should now be able to run the end-to-end test suite by invoking it with the corresponding npm script:
npm run e2e
This will start up a lite-server instance (without starting up its default browser), and protractor will run the tests and exit.
At the top of the app.po.ts
page object file, you imported three targets from Protractor: browser
, element
, and by
. Here's a bit about these targets:
browser
is a protractor global object that allows you to perform browser-level actions, such as visiting URLs, waiting for events to occur, and taking screenshots.element
is a global function that takes a Locator
and returns an ElementFinder
. ElementFinder
is the point of contact to interact with the matching DOM element, if it exists.by
is a global object that exposes several Locator
factories. Here, the by.css()
locator factory performs an analogue of document.querySelector()
.The entire Protractor API can be found at .
The reason for writing the tests this way may feel strange to you. After all, it's a real browser running a real application, so it might make sense to reach for DOM methods and the like.
The reason for using the Protractor API instead is simple: the test code you are writing is not being executed inside the browser runtime. Instead, Protractor is handing off these instructions to Selenium, which in turn will execute them inside the browser and return the results. Thus, the test code you write can only indirectly interface with the browser and the DOM.
The purpose of this recipe was to assemble a very simple end-to-end test suite so that you can get a feel of what goes on behind the scenes in some form. While the tests themselves will appear more or less as they do here, regardless of the test infrastructure they are running on, the infrastructure itself is far from being optimal; a number of changes and additions could be made to make it more robust.
When running unit tests, it is often useful for the unit tests to detect the changes in files and run them again immediately. A large part of this is because unit tests should be very lightweight. Any dependencies on the rest of the application are mocked or abstracted away so that a minimal amount of code can be run to prepare your unit test environment. Thus, there is little cost to running a suite of unit tests in a sequence.
End-to-end tests, on the other hand, behave in the opposite way. They do indeed require the entire application to be constructed and run, which can be computationally expensive. Page navigations, resetting the entire application, initializing and clearing authentication, and other operations that might commonly be performed in an end-to-end test can take a long time. Therefore, it doesn't make as much sense here to run the end-to-end tests with a file watcher observing for changes made to the tests.