Upcoming Course: Code Your Own Business w/ React + GraphQL!
We're live-coding on Twitch! Join us!
Angular  End To End Testing

Angular End To End Testing

End to End testing is a methodolgy used to test an application from a user's perspective. The tests ensure the application performs as expected from start to finish. As the tests run, you will see the browser interaction just as a user would use your application. Angular end to end tests are powered by a framework called Protractor.

Protractor is an end-to-end test framework for Angular applications. Protractor runs tests against your application running in a real browser, interacting with it as a user would.

Protractor runs on top of Selenium Webdriver which is an API for browser automation and testing.

In this blog post we will learn:

Preview of our application

We will be using an application that I have built beforehand. You can go ahead and clone it from my Github repository. We will make reference to it from the page objects section.

Below is a video of the tests running on the browser.

The application has a homepage and an albums page. We will be testing both pages for elements and their funtionality.

Configuring and running end to end tests

When using the Angular CLI, the directory structure is as follows

To run the tests protractor depends on two files. The spec files inside the e2e folder and the protractor.conf.json file. Let's go ahead and look into the protractor.conf.json file

// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/_*/_.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

The line directConnect: true allows Protractor to connect to the browser drivers. The supported browsers at the moment are Chrome, Firefox, Safari, and IE. If you are ok with running the tests on the browser you can leave this file as is. If you are running a different browser from the ones named above you will need to run the Selenium server. The steps are as follows:

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/_*/_.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: false,
  baseUrl: 'http://localhost:4200/',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

For our case we will use the browser drivers. Therefore we will not make any changes to the protractor.conf.js file.

To run the tests simply enter the command ng e2e.

Jasmine

Jasmine is an automated testing framework for JavaScript. In our spec files we will be writing tests using the Jasmine syntax. If you are unfamiliar with the syntax please refer to this page first. Here is a basic example of the jasmine syntax

describe('Exciting App', () => {

  beforeEach(() => { 
  });

  it('should display welcome message', () => {
    browser.get('/home');
    expect(element(by.id('page-title').getText()).toEqual('Welcome to app!!');
  });
});

The describe block is called a suite. This is basically a component of the application. Describe blocks break down the tests into logical test suites. A describe block can have a beforeEach() method which contains code that is run before every test. There can be many it blocks within a describe block. These are the different specs that need to be tested. In the example above the spec tests that the welcome message is displayed. Expectations are built with the function expect which takes a value (the actual). It is chained with a Matcher function, which takes the expected value. If the actual and expected values match then the test passes and fails if they don't. Inside the expect function we have element(by.id('page-title').getText()). We will go through this syntax shortly.

Protractor API

In our above example, inside the it block we noticed we had browser.get('/home'); and element(by.id('page-title').getText(). These are part of the protractor API.

Let's have a look at some of the globals from the protractor API

browser is used for browser level commands such as navigation with browser.get element function is used for finding HTML elements on web pages. It returns an ElementFinder object which can be used to interact with or find information about the element. element takes one argument, a Locator, which describes how to find the element.

Here are some of the Locators we will be using in this tutorial

by.css('') : Used to find an element based on the css selector

by.tagName('') : Used to find an element based on the tag name

To learn more about Locators, have a look here

Page Objects

If you create a project using the angular cli and look at the e2e folder you will notice there are two files We have the spec file that protractor uses to run the tests. Then we have the .po.ts file. This is our page objects file.

In our project the e2e folder structure is a little different. I have made them to match our app folder which is actually good practice. This arranges our tests in an orderly fashion. Here's our folder structure:

Page Object is a Design Pattern which has become popular in test automation for enhancing test maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your AUT. The tests then use the methods of this page object class whenever they need to interact with that page of the UI. The benefit is that if the UI changes for the page, the tests themselves don’t need to change, only the code within the page object needs to change. Subsequently all changes to support that new UI are located in one place.

Let's have a look at our home-page.po.ts file

// e2e/pages/home-page/home-page.po.ts

import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';

export class HomePage {

    navigateToHome(): promise.Promise<any> {
        return browser.get('/home');
    }

    getPageBrandName(): promise.Promise<string> {
        return element(by.css('.masthead-brand')).getText();
    }

    getNavBar(): ElementFinder {
        return element(by.tagName('nav'));
    }

    getAlbumButton(): ElementFinder {
        return this.getNavBar().all(by.css('a')).get(1);
    }

    getLearnMoreButton(): ElementFinder {
        return element(by.css('.lead a'));
    }
}

From our file we can see that in the HomePage class, we have different methods we use to find elements in the page. From our spec file we can then call these methods to test the various elements. Let's get straight into it then. Let's look at our home-page.e2e-spec.ts file

// e2e/pages/home-page/home-page.e2e-spec.ts

import { browser } from 'protractor';
import { HomePage } from './home-page.po';

describe(' Home Page', () => {
    const homePage = new HomePage();

    beforeEach(() => {
        homePage.navigateToHome();
    });

    it('Should have the page brand name', () => {
        expect(homePage.getPageBrandName()).toEqual('Plane Spotters');
    });

    it('Should locate the nav bar', () => {
        expect(homePage.getNavBar()).toBeDefined();
    });

    it('Should get the album button on the nav bar', () => {
        expect(homePage.getAlbumButton().getText()).toEqual('Album');
    });

    it('Should redirect to the album page when album is clicked', () => {
        const album = homePage.getAlbumButton();
        album.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });

    it('Should find the learn more button', () => {
        expect(homePage.getLearnMoreButton().getText()).toEqual('Learn more');
    });

    it('Should redirect to the album page when learn more is clicked', () => {
        const learnMore = homePage.getLearnMoreButton();
        learnMore.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });
});

The tests in the spec file are written using the jasmine syntax we talked about earlier. In the decribe block we first create an instance of the HomePage class we created earlier in the homepage.po.ts file. Then in the beforeEach() method we navigate to the home page. This just makes sure we are on the home page before any test is run. Notice we didn't use any browser.get() method, we had already done that in the page object file. All we have to do now is call that method like this homePage.navigateToHome();

From the page objects file, we are able to locate the elements on the page. When running the tests, we can perform different actions on those elements such as click() , getText() and sendKeys(). A good example is this

    it('Should redirect to the album page when album is clicked', () => {
        const album = homePage.getAlbumButton();
        album.click();
        expect(browser.driver.getCurrentUrl()).toContain('/album');
    });

We get the album button then perform a click action on it. On the browser when we click on album we should be redirected to the album page. Therefore in our expect statement, we check if the URL changed to the album page. There are many expect conditions that you can test for such as toBeDefined(), toEqual() and toContain(). You can get more about jasmine expectations here.

In our album-page folder we have tests written in a similar. Here is our album-page.po.ts file

// e2e/pages/album-page/album-page.po.ts

import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
import { Element } from '@angular/compiler';

export class AlbumPage {

    navigateToAlbumPage(): promise.Promise<any> {
        return browser.get('/album');
    }

    getHomeNavigationButton(): ElementFinder {
        return element(by.css('.navbar-brand'));
    }

    getImages(): ElementArrayFinder {
        return element.all(by.css('.row div'));
    }

    getImagePopUpModal(): ElementFinder {
        return element(by.tagName('app-modal'));
    }
}

And below is our album-page.e2e-spec.ts file

// e2e/pages/album-page/album-page.e2e-spec.ts

import { browser } from 'protractor';
import { AlbumPage } from './album-page.po';

describe('Album Page', () => {

    const albumPage = new AlbumPage();

    beforeEach(() => {
        albumPage.navigateToAlbumPage();
    });

    it('Should find the home navigation button', () => {
        expect(albumPage.getHomeNavigationButton().getText()).toContain('Home');
    });

    it('Should redirect to the home page when \'Home\' is clicked', () => {
        const homeButton = albumPage.getHomeNavigationButton();
        homeButton.click();
        expect(browser.driver.getCurrentUrl()).toContain('/home');
    });

    it('Should have 6 images in the album page', () => {
        expect(albumPage.getImages().count()).toEqual(6);
    });

    it('Should open up a modal when an image is clicked', () => {
        const firstImage = albumPage.getImages().get(0);
        firstImage.click();
        expect(albumPage.getImagePopUpModal().isDisplayed()).toBeTruthy();
    })
});

Conclusion

Having gone through the tests, when we run the command ng e2e we notice our application being run on a browser and on the terminal we notice all the tests pass.

In this article we have covered a lot. We started off with an explanation of what end-to-end tests are, then talked about protractor and jasmine. We also covered page objects and their importance when writing end-to-end tests.

I hope you have learnt a lot and you can now write your own end-to-end tests. Please let me know if you have any questions or comments.

Happy testing!!