left-icon

Angular Testing Succinctly®
by Joseph D. Booth

Previous
Chapter

of
A
A
A

CHAPTER 8

E2E Testing with Jasmine

E2E Testing with Jasmine


Unit tests in the Angular framework are written using the Jasmine library. You can also use the same Jasmine framework commands for writing your end-to-end (E2E) testing code. In this chapter, we will cover how to update E2E configuration files and write E2E tests.

E2E tests rely on a testing library called Protractor, which reads your scenario files and runs them in the browser. When your test files are written in Jasmine or Cucumber (covered in the next chapter), Protractor is the library to run the actual tests.

Note: Protractor uses Selenium Server to run the actual test code. The Protractor installation includes a helper tool called webdriver-manager to get the server running.

Protractor will be installed as part of a project created by the CLI. You can also manually install it using the following command:

npm install -g protractor

You can run the following command to check that Protractor is installed properly, and is the latest version (5.x as of this writing):

protractor –version

Configuration

Protractor must be configured with several options, including the specification files you want to run, the baseUrl (typically localhost:4200) for your application, and the framework name (jasmine). Code Listing 25 is a sample configuration file for Protractor generated by the Angular CLI.

Code Listing 25: protractor.config.js

exports.config = {specs: [

    './src/**/*.e2e-spec.ts'

  ],

  capabilities: {

    browserName: 'chrome'

  },

  directConnect: true,

  baseUrl: 'http://localhost:4200/',

  framework: 'jasmine',

};

Although Protractor can work with different drivers for the browser, we will cover the direct connection option, which works with Firefox and Chrome. This is the default setup for the E2E folder created by the Angular CLI.

Within an Angular project, there will be an E2E folder, which contains Protractor.config.js and a tsConfig.json file. The tsConfig.json file extends the tsConfig file from the parent folder and tweaks a few options for use when Protractor is running. Code Listing 26 shows the default tsConfig file.

Code Listing 26: Default E2E tsConfig.json file

{

    "extends""../tsconfig.json",

    "compilerOptions": {

      "outDir""../out-tsc/e2e",

      "module""commonjs",

      "target""es5",

      "types": [

        "jasmine",

        "jasminewd2",

        "node"

      ]

    }

  }

All the file does is adjust the compiler options and output folder to allow E2E to run.

Protractor.config.js

This file is the primary source of settings for Protractor to run. The file exports a configuration object with settings necessary for Protractor. You can view all available settings here.

Defining browsers

Each browser you want to test in is included in the capabilities section of the configuration file. If you’re only testing one browser, the section will contain an object. If you’re testing multiple browsers, the section name must be multiCapabilities, and should be an array of objects, one per desired browser. Code Listing 27 shows a sample configuration to run both Chrome and Firefox browsers.


Code Listing 27: multiCapabilities section

multiCapabilities: [   {  browserName: 'chrome' },

                       {  browserName: 'firefox'}

                     ],

Note that you might need to update your webdriver manager to make sure alternate browsers work properly. You can do this by running the following command in your project folder:

node node_modules\protractor\bin\webdriver-manager update

Now, when you run the E2E command, the tests will be run in all browsers specified.

Firefox

Firefox cannot access the browser logs, so the function afterEach() in the spec files needs to check for the browser name before attempting to read the logs. Code Listing 28 shows the revised afterEach() function.

Code Listing 28: Revised afterEach function

if (browserName != 'firefox') {

    const logs = await browser.manage().logs().get(logging.Type.BROWSER);

    expect(logs).not.toContain(jasmine.objectContaining({

      level: logging.Level.SEVERE,

    } as logging.Entry));

  }

You will need to get the current browser name during the beforeAll() function. Code Listing 29 shows the beforeAll() function.

Code Listing 29: beforeAll function

beforeAll(() =>{

    browser.driver.getCapabilities().then(function(caps){

      browserName = caps.get('browserName');

    });

  })

While it might not be necessary to know the browser name in all instances, this is a good example of using the beforeAll() method to define some variables you might need in your specification files.

Jasmine options

The JasmineNodeOpts section lets you set options to configure how Jasmine will run. Table 1 shows a list of the possible Jasmine options.

Table 1: Jasmine options

Option

Description

showColors

If true, print colors to terminal.

defaultTimeoutInterval

Timeout in milliseconds before a test will fail.

print

Function to call to print Jasmine results.

grep

Grep string to filter specification files to run.

invertGrep

If true, inverts the grep to exclude files.

random

If true, run the tests in random order.

seed

String to set seed to if using random.

The Angular-CLI-generated file sets the showColors and defaultTimeoutInterval values, and generates an empty print function.          

Test files

Once you’ve configured your Protractor setup (or used the Angular-generated one), you need to explore the actual specification test files that will be used to run the E2E tests. These can be found in the src directory of the e2e folder. You will generally find two files, one called app.po.ts, which declares the application page object (called AppPage), and the actual spec file, app.e2e-spec.ts, which contains tests to run.

App.po.ts

This TypeScript file will define the application page and provide methods for common actions, such as navigating to a URL, getting elements from the screen, and interacting with elements.

Code Listing 30 shows the default Angular-CLI-generated appPage class.

Code Listing 30: Default app.po.ts

import { browserbyelement } from 'protractor';

export class AppPage {

  navigateTo() {

    return browser.get(browser.baseUrlas Promise<any>;

  }

  getTitleText() {

    return element(by.css('app-root .content span')).getText() as Promise<string>;

  }

}; 

This file includes some modules from the Protractor API (browser, by, and element) and provides a couple of method calls to navigate (using the browser object) and search for an element (using the by object) using some of the element’s properties.

Typically, you will add additional methods to this file to simplify and provide common methods for your specification files.

Protractor API

To write your E2E tests, you will be using the objects from the Protractor API. This API allows you to access the browsers, and to grab elements from the browser screen. You can get the text of the element, click an element, send text to the element, and so on. All the operations your user can do manually are available to the API.

browser()

The browser object provides commands to control the browser running the E2E tests. It is browser agnostic, so the commands should work for all browsers you configure your tests to run on. The Protractor browser class inherits from the webdriver-manager class and adds methods and properties for E2E testing. You can find the complete documentation for the browser class here.

Navigation

The browser object provides a get() command, which is used to navigate to a URL. For example, Code Listing 31 provides example code (which could be added to the AppPage object) that allows you to navigate to the home (baseUrl), a page within the application, or an external URL.

Code Listing 31: Navigation methods

navigateTo() {

    return browser.get(browser.baseUrlas Promise<any>;

  }

  navigateToPage(urlstring ) {

    return browser.get(browser.baseUrl+'/'+urlas Promise<any>;

  }

  navigateToUrl(urlstring ) {

    return browser.get(urlas Promise<any>;

  }

actions()

actions() is a method that allows you to chain together steps to perform in the browser. For example, you might want to test moving the mouse up or down or test a drag-and-drop operation. You build a chain of action methods, but they are not executed until the perform() action is executed.

The following is a list of actions that can be chained together:

  • click()
  • doubleClick()
  • dragAndDrop()
  • keyDown()
  • keyUp()
  • mouseDown()
  • mouseMove()
  • mouseUp()
  • sendKeys()

You create a chain of actions, then add the perform() action to execute the defined list of steps. If you want to perform a drag-and-drop test, you will need to find two elements using the element methods.

Code Listing 32 shows example code to show a drag-and-drop operation in your test file.

Code Listing 32: DragAndDrop example code

  let tgt = element(by.id("Amt"));

  let src = element(by.id("Fifty"));

    browser.actions().dragAndDrop(src,tgt);  

This code grabs the element value using the id of “Fifty” and drops its value to the input element using the id of “Amt”. We will cover the element() and by() methods later in this chapter.

TouchActions()

TouchActions() are like the actions method, but allow the chaining of actions related to touch, such as tap and longPress. The actions are chained together, and when a perform() action is reached, the actions are executed. The following touch actions are allowed:

  • doubleTap()
  • flick()
  • flickElement()
  • longPress()
  • move()
  • release()
  • scroll()
  • scrollFromElement()
  • tap()
  • tapAndHold()

Most of the action methods (either regular or touch) will take an element as a parameter. Once the chain is built, the perform() method is added to execute the sequence.

Getting browser information

There are several functions that allow you to retrieve information from the browser, such as capabilities, geolocation, and device time. A few of them are described here. Each method returns a promise, so you need to attach an event handler to receive the result. Code Listing 33 shows example code to capture a promise (the then() function).

getCapabilities()

This method returns an object describing the capabilities of the browser. Code Listing 33 shows how to call the method to gather some information about the browser. Since this information will stay the same during execution of the test, you should consider adding it to the beforeAll() method in the step definition.

Code Listing 33: Browser capabilities

    beforeAllasync () =>{

        browser.getCapabilities().then((c) => {

          hasTouch = c.get('hasTouchScreen');

          browserName = c.get('browserName');      

          platform = c.get('platform');

        });

      });  

getCurrentURL()

This method returns a promise of a string value holding the current URL. You’ll often need this to test expected navigation results.

getGeoLocation()

This method returns an object with the latitude, longitude, and altitude of the device location. There is a corresponding setGeoLocation() method that allows you to set locations, in case you want to test location-specific features of your application.

getScreenOrientation()

This method returns a string with either LANDSCAPE or PORTRAIT, depending upon the device orientation. The setScreenOrientation() method lets you set the orientation mode to test scenarios where the application behaves differently based on orientation.

getTitle()

This method will return the browser title for the current window.

Browser behavior

There are also several browser methods that let you change settings on the browser, primarily for testing different statuses, such as Airplane Mode or no Wi-Fi. These can be particularly useful when creating a test plan for mobile applications.

close()

This method closes the current browser window.

toggleAirplaneMode()

This method allows you to toggle Airplane Mode (disable Wi-Fi, Bluetooth, GPS, and so on). You might want to test local usage of your application with Airplane Mode turned on.

toggleWifi()

This method toggles the Wi-Fi setting, allowing you to test behavior as if Wi-Fi is turned off.

sleep()

The sleep() method takes a parameter of the number of milliseconds the browser should sleep for. You can use it to allow some time for dynamic code to populate the controls on a form or to simulate user behavior.

element()

The element object represents a single element from the screen. It might be some input text, an Angular binding, or a button. In order to get the element, you need to use a locator, which is a method call that provides different ways to find an element. The simplest and most direct locator is by.id(), which lets you get an element associated with the element’s id property. The parameter to the element() method must be a locator, and is expected to return a single element (element.all() expects a collection of elements).

Locators begin with the keyword by, followed by a method name, such as id() or buttonText(). You can find a complete list of locators here.


Some of the more common locators are listed in Table 2.

Table 2: Common locators

Locator

Description

binding(string}

Returns element with interpolated string.

<span>{{ GPA }}  </span>

by.binding(‘GPA’) would return the <span> element.

id(elementId)

Returns the element with the associated id property.

<input id=‘pwd’> 

by.id(‘pwd’) would return the input element

buttonText(text)

Returns button element whose text matches the parameter.

model(name)

Returns element associated with ng-model name.

<input type="text" ng-model="person.name">

by.model(‘person.name’) returns the input element.

name(element name)

Returns element with the given name property.

Use carefully, since multiple elements can have the same name, in which case the first one is returned, and a warning is given.

css(css selector)

Finds an element by a CSS selector, such as H1 (header 1 element) or .Menu element with the class name of Menu.

You can use $() as shorthand for finding elements by CSS.

element.all()

element.all() works similarly to element(), but is expected to return a collection of elements (even if only one element is found, a collection will still be returned). element.all() still requires a selector, although the selector should be a scope larger than id or binding. You might use this method to determine the number of items in a list box, or menu options. Code Listing 34 shows an example of using element.all to retrieve all li tags from a class called menu.

Code Listing 34: Retrieve menu items

let menus = element.all(by.css(‘.menu li’));

Once you have the variable, which is a collection of elements, you can use the methods shown in Table 3 to get details of or manipulate the collection.

Table 3: Element.all methods

Method

Description

get()

Gets an element by the index parameter (zero-based).

count()

Returns number of elements in the collection.

first()

Gets the first element from the collection.

last()

Gets the last element in the collection.

each()

Performs a function on each element in the collection.

map()

Applies a map function to the collection. The function gets the element and index as parameters, and returns a collection of the object built by the map.

You can use $$() as shorthand for element.all(by.css()).

Working with elements

Once you’ve identified an element, you can interact with it to send text, click a button, check value, and so on. You will typically save the found element to a variable, and then process commands against the element.

The following are the common methods on the element.

isPresent()

This method is used to determine whether an element exists on the page. For example, the following code snippet checks to see if an element with the id of SaveBtn is on the current page.

element(by.id(‘SaveBtn’)).isPresent()

isEnabled()

This method will test whether the element is currently enabled. For example, in some applications, a Save button stays enabled until all errors are cleared. This would allow you to confirm that behavior for applications designed using that approach.

isSelected()

This method indicates whether an element is selected or not. The following code snippet shows how we might determine whether taxes should be applied in an e-commerce application.

<input id="ApplyTaxes" type="checkbox">      

var taxes = element(by.id('ApplyTaxes'));

taxes.click();

expect(taxes.isSelected()).toBe(true);

isDisplayed()

This method is used to determine whether the current element is displayed on the screen. Note that if you use the Angular *ngIf directive to control an element, the element is either added to the DOM, or not. The isDisplayed() method should not be used to test *ngIf conditional elements; use isPresent() instead.

getText()

The getText() method returns the value of the element without any leading or trailing spaces. It returns the content of the HTML element’s innerText property. Note that the element must be visible for this method to return the value.

clear()

The clear() method sets the value of the element to an empty string.

click()

This method clicks the element, either by selecting the element (such as an input field) or performing the associated action (such as with buttons or links).

submit()

This method should be on a form element and will perform the submit action associated with the form.

sendKeys()

This method allows you to “type” text characters into an editable HTML element. Note that the text will be appended, so you’ll need to clear the previous text if you want new text in the element. The following code snippet shows an example of the method.

enterNewText(id:string, text:string) {

    let elt = element(by.id(id));

    elt.click();

    elt.clear();

    elt.sendKeys(text);

  }

This example clicks in the element, clears the previous text, and adds the new text value to the control.

getCssValue()

This method returns the computed style for an element and property. The method expects a CSS property value as a string parameter. It will return the computed style for the element, using the CSS priority logic to determine the style. For example, you might want to use this method to determine an element’s color, expecting red (#FF0000) when an error condition has occurred. The following snippet checks to make sure the error message is displayed in red text.

<div id="ErrorMsg">Transaction failed</div>

var errMsg = element(by.id('ErrorMsg'));

expect(errMsg.getCssValue(‘color’).toBe(‘#FF0000’);

Note that colors will be returned as hex values, regardless of how the color was computed.

getAttribute()

This method returns the current value for an attribute on the element. Attributes that are Boolean (such as autofocus, checked, and selected) will be returned as either true (attribute is set on the element) or null (the attribute is not on the element). The class attribute returns a string value of the element’s current class. The style attribute returns a representation of the current style settings as a delimited string.

getTagName()

This method returns the HTML tag that the element was found within. For example, if we wrapped the error message in a <span> tag, the method would return the text span.

App.e2e-specs.ts

This is the primary file where your specifications are written. These should typically be written so that a non-developer can understand them. The clearer your method names in po.ts are, the clearer the spec will read.

We discussed Jasmine a bit in Chapter 6 in terms of unit testing. Jasmine is also used for E2E testing. Combined with the Protractor methods to control the browser, we can mimic the behavior of a user and define expected outcomes.

describe()

Typically, you want to tie the E2E test to a user story (an Agile framework artifact describing the functionality from a user point of view). Your spec files will begin with a describe function, generally some test that indicates which user story you are testing. In our banking example, we might have a user story as shown in Code Listing 35.

Code Listing 35: Simple user story

As a “Bank Customer”

I want “To withdraw cash”

So that “I can have a fun night out”

With this story, we can create a describe function such as:

describe(“Withdraw cash from ATM”, () => {} )

It is possible to create a single spec file for your entire application, but I would recommend keeping the spec files small and focused on one part of the application.

Note: Job stories are an alternate to user stories, focusing on an event, such as “when reviewing order history.” In this case, the user reviewing the history is not as important as the action being done. The format of the job story is: When <situation> I want to <action> so I can <expected outcome>. Whatever story format your analysts use, be sure it is clear in your test specs which story the test is related to.

it()

The it() method is where you will describe each part of the story being tested. You will use the various Protractor browser and element methods to make the browser mimic the behavior you’d expect the user to perform. It takes a string (description test name) and a function to perform the test. The description name should provide a person reading the spec a clear explanation of what part of the user story is being tested. For example, for the “withdraw cash from ATM” story, you might have test methods like:

it(“Allow the user to withdraw cash”)

it(“Display an error for insufficient funds”)

it(“Display an error if trying to withdraw more than daily limit:”)

Keep in the mind that the test spec file is also documentation for the business analyst, so even though they might not understand the code, using good descriptions and methods names will go a long way.

Structure

The generated spec file, and in general, the ones you write as well, will follow the structure of the code. The script should consist of describe and an anonymous function:

describe(“User story description”, () => {

}

The first part of the function should declare the variables to be used through the script. At minimum, a variable will be created from the class defined in the po.ts file. Any additional variables needed should be defined as well. Code Listing 36 shows a simple variable declaration list and a beforeAll method (called once before the tests start) to populate some variables.

Code Listing 36: Partial spec file (startup)

describe('Withdraw cash from ATM', () => {

        let page: AppPage;

        let browserName: string;

        let hasTouch: boolean;

        let platform: string;

        let BankBalance: number;

        beforeAllasync () =>{

          browser.getCapabilities().then((c) => {

            hasTouch = c.get('hasTouchScreen');

            browserName = c.get('browserName');      

            platform = c.get('platform');

          });

        });

You can initialize other variables the test might need. In our example, we are creating the object and declaring a bank balance variable. We also update a few browser properties we might use in the test.

Code Listing 37 shows the beforeEach method, which is called prior to each individual test being run. For each test, we create a new copy of the AppPage object and reset the bank balance to $1,000.

Code Listing 37: beforeEach method

beforeEach(() => {

            page = new AppPage();    

            bankBalance = 1000;

          });

Once we’ve set up our variables and before methods, we can write our actual tests using the Protractor methods to run the test. Keep in mind that the po.ts file should provide some wrapper functions to make the code clearer. For example, the following code snippet will find an element and enter text into it.

let elt = element(by.id(id));

    elt.click();

    elt.clear();

    elt.sendKeys(text);

However, to assist the business analysts, I’ve added a wrapper function around those commands called enterText(id, text). This is a bit more readable to the spec reader.

page.enterText("amount","250");      // Enter $250

In a similar fashion, my po.ts file contains definitions for clicking a button and menu link, as shown in Code Listing 38.

Code Listing 38: po.ts wrapper functions

      enterText(id:stringtext:string) {

            let elt = element(by.id(id));

            elt.click();

            elt.clear();

            elt.sendKeys(text);

      }

      clickMenuLink(idstring) {

         return element(by.id(id)).click();

      }

      clickButton(idstring) {

         return element(by.id(id)).click();

      };  

      async resultMsg() {

            let elt = element(by.id('resultMsg'));

            return elt.getText();

      };   

Even though the clickMenuLink and clickButton methods contain identical code, by using different method names, we allow the end user to read the definition in the test steps and get a better understanding of what the test is doing (such as understanding if this step is clicking on a menu or a button). Code Listing 39 shows the test to withdraw money.

Code Listing 39: Withdraw money example

it('Should allow me to withdraw $250 from checking'async () => {

            page.clickMenuLink("withdraw");       // Go to withdrawal page

            page.enterText("amount","250");       // Enter $250

            page.clickButton("withdrawMoney");

          }); 

While the method shows the action, we still need to see if the test worked. We call our withdraw component method to get a Boolean true or false value back. Code Listing 40 shows the completed method.

Code Listing 40: Completed withdraw $250 test

it('Should allow me to withdraw $250'async () => {

            page.clickMenuLink("withdraw");        // Go to withdrawal page

            page.enterText("amount","250");        // Enter $250

            page.clickButton("withdrawMoney");    

            expect(page.resultMsg()).toBe(‘OK’);

          });

Once the sequence of steps is performed, expect() is evaluated to determine whether the test passed. In this case, expect the status message to be OK, since the withdrawal was allowed.

Code Listing 41 shows the failed withdrawal case, attempting to withdraw $1,250 instead of $250.

Code Listing 41: Failed withdrawal test

it('Should not allow me to withdraw $1250'async () => {

            page.clickMenuLink("withdraw");        // Go to withdrawal page

            page.enterText("amount","1250");       // Enter $1250

            page.clickButton("withdrawMoney");

            expect(page.resultMsg()).toBe("Insufficient funds");

          });

Keep your test simple and readable. The more you hide the programming complexity inside the po.ts file, the easier the spec file should be for business analysts to read.

afterEach method

The afterEach() method is called upon completion of each test (it() method) in the spec file. This can be useful for any cleanup work, or checking the browser logs for errors, like the default code from the Angular CLI does. Code Listing 42 is the afterEach() method from the spec.

Code Listing 42: afterEach method

afterEach(async () => {

  if (browserName != 'firefox') {

      const logs = await browser.manage().logs().get(logging.Type.BROWSER);

      expect(logs).not.toContain(jasmine.objectContaining({

           level: logging.Level.SEVERE,

         } as logging.Entry));

      }

 });

Note that the Firefox browser does not allow access to the logs, so by using the browser name variable we populated during the beforeAll() method, we check to only review the logs for errors if we are not using Firefox.

Summary

E2E testing, while not a final test (QA departments are still needed), provide a good sense that the overall application flow is working. By creating good method names inside your po.ts file, you can produce a readable specification file that can also drive your tests. While it takes a bit of effort to set up, and can take some time to run, it is a time-saver for an overall testing plan. This allows QA to focus on subtle bugs, or application bugs where the specification doesn’t quite do what is expected. And whenever QA does find a bug, you can add it to your E2E tests to be sure it is caught the next test run, before QA gets their hands on it.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.