Angular E2E Protractor Introduction

This is an introduction to Angular E2E tests with two examples using Angular 10. The first example is the default test generated by the Angular 10 CLI and the second test does the following:

  1. Logs in
  2. Creates an item
  3. Modifies the item
  4. Deletes the item
  5. Logs out

Angular uses Protractor to do the e2e tests. If you generate a project with the Angular CLI it's very simple to run your e2e tests by simply running ng e2e.

Source code can be found at https://github.com/saaratrix/angular-e2e-example

Why should I use E2E tests?

E2E tests can be useful to automate a sequence of actions or that everything works as it mimics a user browsing the website. For example you could have E2E tests that does the most common things like login, interact with something and logout. You can then assume that those cases work without having to manually test it yourself every time. In the protractor.conf.js file you can set the URL you're testing so you could test directly against your production server.

The default Angular 10 CLI example

If you created an angular application using the cli it should look something like in this image:

The e2e tests goes to the e2e folder. If you want a different folder make sure it's included in the protractor.conf.js file.

Running the tests

You can run the tests by doing the command ng e2e or npm run e2e. A browser will then open that runs the tests.

Running all the tests using ng e2e can be very time consuming for a larger app. This is because it has to compile the whole application before running the tests and to start a browser. It can also be time consuming because you have many tests or tests that take a lot of time to run.

In Webstorm you can drastically reduce the time spent developing E2E tests by running specific tests. Webstorm does this by skipping the compiling part and instead uses the ng serve build. There might be a similar feature in VS Code or other editors to run a specific test. Here's how to run a specific test in Webstorm:
Yes this is an unsponsored ad for Webstorm!!

  1. Run the app normally with ng serve. If you don't do ng serve you'll get a message that the website could not be reached when running the test.
  2. Click on the green arrow to run as specific test.

The default test's code

When you look at the default test's code you might notice that it looks a lot like a unit test. The e2e test is using jasmine and if you want to change that you can do that in the protractor.conf.js file.

app.e2e-spec.ts

Before each test it creates our Page Object AppPage. More about the page objects later but in short it's a design pattern to handle the DOM interactions or a sequence of actions. Then the test navigates to the app page and checks that the title text is as expected. Finally after each test it checks for any error messages in the console log.

import { AppPage } from './app.po';
import { browser, logging } from 'protractor';

describe('workspace-project App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getTitleText()).toEqual('angular-e2e-example app is running!');
  });

  afterEach(async () => {
    // Assert that there are no errors emitted from the browser
    const logs = await browser.manage().logs().get(logging.Type.BROWSER);
    expect(logs).not.toContain(jasmine.objectContaining({
      level: logging.Level.SEVERE,
    } as logging.Entry));
  });
});

app.po.ts

In the AppPage's page object there is code that is only used on the app page. Which in this example is navigate to the app page and get the page's title text.

import { browser, by, element } from 'protractor';

export class AppPage {
  navigateTo(): Promise<unknown> {
    return browser.get(browser.baseUrl) as Promise<unknown>;
  }

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

What are Page Objects?

Page object is a design pattern to write reusable e2e test code. As seen in the test above it has methods that navigates you to the page, or get an element and the text. It makes it simple to write more e2e tests because you can now easily reuse the functionality. For example if you write login and logout methods you can now use them in all the tests.

Page object file names typically end with .po.ts.

Some links about page objects for further reading:
https://github.com/SeleniumHQ/selenium/wiki/PageObjects
https://www.protractortest.org/#/page-objects
https://martinfowler.com/bliki/PageObject.html

protractor.conf.js

This is the file that tells protractor what files to run, what browser to use and what testing framework and more. If you have very long tests you need to either change the jasmineNodeOpts.defaultTimeoutInterval variable.

Or pass the duration to the it() method.

it('a test', function() {
    // test code ...
}, 10000); // 10 seconds

The protractor.conf.js file when generated with Angular 10 CLI:

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

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

/**
 * @type { import("protractor").Config }
 */
exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './src/**/*.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: require('path').join(__dirname, './tsconfig.json')
    });
    jasmine.getEnv().addReporter(new SpecReporter({
      spec: {
        displayStacktrace: StacktraceOption.PRETTY
      }
    }));
  }
};

Log in, handle an item and log out example

The example's source code an be found here: https://github.com/saaratrix/angular-e2e-example.

Here is how the test looks like when using npm run e2e.

The test's spec file can be found here: https://github.com/saaratrix/angular-e2e-example/blob/main/e2e/src/app.e2e-spec.ts. It does the following:

  1. First we login
  2. Then we create an item
  3. We assert that an item was created by checking item count.
  4. We delete an item
  5. We assert that the item was deleted by checking item count.
  6. We logout
  7. We assert that the login button is visible.

By using the page object pattern we get very little code in the test itself and it's easy to follow each step.

it('should login, create item, edit item, delete item and then log out.', () => {
    loginPage.login('saaratrix', '123456');
    appPage.createItem('item Nuu');
    // Used for asserts
    const currentItems = appPage.getItems();
    expect(currentItems.count()).toBe(1);
    appPage.updateItem('item Nuu', 'modified Nuu');
    appPage.deleteItem('modified Nuu');
    expect(currentItems.count()).toBe(0);
    loginPage.logout();

    expect(loginPage.getSignInButton().isDisplayed()).toBe(true);
});

Page object selectors

When creating the e2e tests you need to know either how to use CSS Selectors or XPath to get the elements but CSS Selectors are recommended because of their readability. And sometimes you might need to update the HTML to easier select a button, an input field etc. But you can test the CSS selector in the browser's developer tools by in the console writing document.querySelector('selector query'). And for XPath you can use $x('xpath query') in both Chrome and Firefox.

Tips & Tricks

  • Protractor is not executed synchronously. The test code may look synchronous but everytime you do a browser. call you're adding a promise to some internal chain. So for example if you want to calculate the time it takes between browser.wait() calls in a test you can use .then() on each promise.
  • If you see the error message Invalid locator it's very likely you forgot to write by.css() or another method inside the element().
  • If you want to get the parent of an element you can do that with XPath (by.xpath('..')). element(by.css('.child')).element(by.xpath('..'));. I have tried to find a better way but this is the simplest way that I know of so far.

No comments:

Post a Comment