© Fu Cheng 2018
Fu ChengBuild Mobile Apps with Ionic 4 and Firebasehttps://doi.org/10.1007/978-1-4842-3775-5_14

14. End-to-End Test and Build

Fu Cheng1 
(1)
Sandringham, Auckland, New Zealand
 

In the previous chapters, we focused on the implementation of different user stories and the Ionic framework itself. In this chapter, we’ll focus on some nonfunctional requirements, including end-to-end test with Protractor and build with Headless Chrome and GitLab CI. These steps are essential for real-world app development. After reading this chapter, you should know how to apply these practices.

End-to-End Test with Protractor

Protractor ( http://www.protractortest.org/ ) is an end-to-end test framework for Angular applications. The name “Protractor” comes from the instrument that is used to measure angles, indicating its close relationship with Anguar. Most of the end-to-end testing tools are using Selenium WebDriver ( http://www.seleniumhq.org/projects/webdriver/ ) to drive a browser as a user would. You can use the WebDriver API directly, or use tools like Nightwatch.js ( http://nightwatchjs.org/ ) or WebdriverIO ( http://webdriver.io/ ), which wrap the WebDriver API to provide an easier-to-use API. Protractor also uses WebDriver API, but it has a close integration with Angular to make find elements much easier.

We use npm to install Protractor and the dependent package webdriver-manager that manages the Selenium server for testing.
$ npm i -D protractor webdriver-manager
Then we need to update webdriver-manager to install drivers for different browsers. This is done by running the command update of webdriver-manager. Because we didn’t install the package webdriver-manager globally, we must use the full-qualified path to access the binary of webdriver-manager.
$ node_modules/webdriver-manager/bin/webdriver-manager update
After the command update is finished, we can start the Selenium server using the command below.
$ node_modules/webdriver-manager/bin/webdriver-manager start

To make sure that webdriver-manager is updated, we can add a postinstall script to package.json to run webdriver-manager update after the packages installation.

Protractor Configuration

Protractor is configured using the file protractor.conf.js in the directory e2e; see Listing 14-1. This file contains different settings. Refer to this page ( http://www.protractortest.org/#/api-overview ) for more information about the configuration.
  • seleniumAddress - Set the Selenium server address. http://localhost:4444/wd/hub is the default Selenium address, so the property seleniumAddress can be omitted.

  • allScriptsTimeout - Set the timeout settings to 15 seconds.

  • specs - Set the pattern to find all specs files. Here we use all the files with the suffix .e2e_spec.ts in the directory src.

  • capabilities - Set the capabilities requirements for Selenium server. Here we require using Chrome.

  • directConnect - Connect directly to the browser without using a Selenium server. This option can be enabled for Chrome and Firefox to speed up the execution of test scripts. When this property is set to true, Selenium-related settings are ignored.

  • baseUrl - Set the base URL for testing.

  • framework - Set the testing framework. Jasmine is the recommended framework.

  • jasmineNodeOpts - Set the options for Jasmine.

  • beforeLaunch - Set the action to run before any environment setup. Here we register the directory e2e to ts-node, so ts-node compiles TypeScript code on-the-fly during the execution of the script.

  • onPrepare - Set the action to run before the specs are executed. Here we configure Jasmine to add one reporter. SpecReporter is used to output the testing result to the console.

const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
  allScriptsTimeout: 15000,
  specs: [
    './src/**/*.e2e-spec.ts'
  ],
  capabilities: {
    browserName: 'chrome',
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  beforeLaunch() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
  },
  onPrepare() {
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
    browser.waitForAngularEnabled(false);
    // The css_sr selector is copied from https://github.com/angular/protractor/issues/4367.
    // The related PR (https://github.com/angular/protractor/pull/4786/) is not merged yet.
    by.addLocator('css_sr', (cssSelector, opt_parentElement, opt_rootSelector) => {
      let selectors = cssSelector.split('::sr');
      if (selectors.length === 0) {
        return [];
      }
      let shadowDomInUse = (document.head.createShadowRoot || document.head.attachShadow);
      let getShadowRoot  = (el) => ((el && shadowDomInUse) ? el.shadowRoot : el);
      let findAllMatches = (selector, targets, firstTry) => {
        let using, i, matches = [];
        for (i = 0; i < targets.length; ++i) {
          using = (firstTry) ? targets[i] : getShadowRoot(targets[i]);
          if (using) {
            if (selector === “) {
              matches.push(using);
            } else {
              Array.prototype.push.apply(matches, using.querySelectorAll(selector));
            }
          }
        }
        return matches;
      };
      let matches = findAllMatches(selectors.shift().trim(), [opt_parentElement || document], true);
      while (selectors.length > 0 && matches.length > 0) {
        matches = findAllMatches(selectors.shift().trim(), matches, false);
      }
      return matches;
    });
  }
};
Listing 14-1

protractor.conf.js

The baseUrl we use for testing is http://localhost:4200, which is the URL of the Angular dev server. In the function onPrepare, we also call browser.waitForAngularEnabled(false) to disable the waiting for pending asynchronous tasks to finish in Angular ( https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular ). Otherwise, all the tests will fail with timeout errors: Timed out waiting for asynchronous Angular tasks to finish after 15 seconds while waiting for element with locator. This is because Firebase uses WebSocket to connect to the server and the connection is treated as in-progress asynchronous task.

In the function onPrepare, we also add a custom Protractor locator css_sr. This selector enables selection of elements inside of shadow trees. Ionic Core components uses Shadow DOM by default. The DOM elements inside of their shadow roots cannot be selected using existing CSS selectors in Protractor. There is already a pending GitHub pull request ( https://github.com/angular/protractor/pull/4786/ ) to add this selector to Protractor. Once this pull request is merged, we no longer need to add it in the function onPrepare.

The file package.json already has the script e2e to run Protractor. We can run npm run e2e or ng e2e to start the Protractor tests.

Top Stories Page Test

After Protractor is configured, we add the first end-to-end test suite for the top stories page. The spec file is written with Jasmine, so we can leverage what we already know in writing unit tests; see Listing 14-2. The first test spec verifies that there should be 20 stories. The method browser.get(“) loads the page in the browser. Because we already configure the baseUrl in protractor.conf.js, the empty string means loading the base URL. The functional call browser.wait(EC.presenceOf(element(by.css('app-top-stories'))), 5000) uses the ExpectedConditions ( https://www.protractortest.org/#/api?view=ProtractorExpectedConditions ) provided by Protractor to wait for certain conditions to be met. The condition presenceOf means the element matching the given CSS selector must exist in the DOM. Here we wait for the element app-top-stories to be created. The method element.all(by.css('app-item')) finds all <app-item> elements, then we verify the number to be 20.

In the second spec, we use the method browser.executeScript() to execute some JavaScript code in the browser. The script document.getElementsByTagName("ion-infinite-scroll")[0].scrollIntoView(); makes the content to scroll to show the component ion-infinite-scroll, which triggers the loading of more items. We then verify the number of items should be 40. The method browser.sleep(5000) makes the tests to sleep 5 seconds to allow the loading to finish.
import { browser, element, by, ExpectedConditions as EC } from 'protractor';
describe('top stories page', () => {
  it('should show 20 stories', () => {
    browser.get(“);
    browser.wait(EC.presenceOf(element(by.css('app-top-stories'))), 5000);
    const stories = element.all(by.css('app-item'));
    expect(stories.count()).toEqual(20);
  });
  it('should show more stories when scrolling down', () => {
    browser.get(“);
    browser.wait(EC.presenceOf($('app-top-stories')), 5000);
    let stories = element.all(by.css('app-item'));
    expect(stories.count()).toEqual(20);
    browser.executeScript('document.getElementsByTagName("ion-infinite-scroll")[0].scrollIntoView();');
    browser.sleep(5000);
    stories = element.all(by.css('app-item'));
    expect(stories.count()).toEqual(40);
  });
});
Listing 14-2

Test spec for the top stories page

Note

The way we use browser.sleep to wait for actions to be finished in Protractor tests is not ideal. Most of Protractor APIs actually return Promise objects that can be easily chained together to write faster and more reliable tests. Protractor also has a good integration with Angular. Unfortunately, we have to use browser.waitForAngularEnabled(false) to get test specs executed. This is a special case for using Firebase. If your app doesn’t have the same issue, you should always use Protractor’s Angular support.

Page Objects and Suites

In the test specs of Listing 14-2, we can see a lot of code duplication. We can refactor the test code using the Page Objects pattern ( https://martinfowler.com/bliki/PageObject.html ). A page object describes a page and encapsulates all logic related to this page. These page objects can be reused in different tests.

In Listing 14-3, TopStoriesPage is the class for the top stories page. The method get() loads the page, scrollDown() scrolls down to trigger the loading of more items, getStoriesCount() gets the number of stories in the page. TypeScript files of page objects usually have suffix .po.ts. Here we use two convenient shortcuts, $ and $$, to replace the usage of element and by in Listing 14-2. $ is the shortcut of element(by.css()), while $$ is the shortcut of element.all(by.css()).
import { $, $$, browser, ExpectedConditions as EC } from 'protractor';
export class TopStoriesPage {
  get() {
    browser.get(“);
    browser.wait(EC.presenceOf($('app-top-stories')), 5000);
  }
  scrollDown() {
    browser.executeScript('document.getElementsByTagName("ion-infinite-scroll")[0].scrollIntoView();');
    browser.sleep(5000);
  }
  getStoriesCount() {
    browser.wait(EC.presenceOf($('app-item')), 5000);
    return $$('app-top-stories app-item').count();
  }
}
Listing 14-3

Page object of the top stories page

Listing 14-4 is the actual test spec for the top stories page. In the method beforeEach(), an object of TopStoriesPage is created for each test spec and used to interact with the page.
import { browser, element, by } from 'protractor';
import { TopStoriesPage } from './top-stories.po';
describe('top stories page', () => {
  beforeEach(() => {
    this.topStoriesPage = new TopStoriesPage();
    this.topStoriesPage.get();
  });
  it('should show 20 stories', () => {
    expect(this.topStoriesPage.getStoriesCount()).toEqual(20);
  });
  it('should show more stories when scrolling down', () => {
    expect(this.topStoriesPage.getStoriesCount()).toEqual(20);
    this.topStoriesPage.scrollDown();
    expect(this.topStoriesPage.getStoriesCount()).toEqual(40);
  });
});
Listing 14-4

Test spec of top stories page

We can also group test specs into different suites. In the protractor.conf.js, we can use the property suites to configure suites and their included spec files; see Listing 14-5.
exports.config = {
  suites: {
    top_stories: './src/top_stories.e2e_spec.ts',
  },
};
Listing 14-5

Protractor test suites

Then we can use --suite to specify the suites to run.
$ protractor --suite top_stories

User Management Test

We continue to add test specs for user management-related features . The class LogInPage in Listing 14-6 is the page object for the login page. In the method get(), we go to the index page and call the method gotoLogin() to navigate to the login page. In the method logIn(), we use sendKeys() to input the test user’s email and password, then click the button to log in. Here we use the selector css_sr added in Listing 14-1 to select the input elements inside of ion-input components. ::sr in the css_rs is used to separate the selector used in the main DOM tree and selector used in the shadow root. In the method canLogIn(), we check the existence of the login button. In the method isLoggedIn(), we check the existence of the logout button. In the method logOut(), we click the logout button to log out the current user.
import { browser, element, by, $ } from 'protractor';
export class LogInPage {
  get() {
    browser.get(“);
    browser.sleep(5000);
    this.gotoLogin();
  }
  gotoLogin() {
    $('#btnShowLogin').click();
    browser.sleep(2000);
  }
  logIn(email: string = '[email protected]', password: string = 'password') {
    element(by.css_sr('ion-input::sr input[name=email]')).sendKeys(email);
    element(by.css_sr('ion-input::sr input[name=password]')).sendKeys(password);
    $('#btnLogin').click();
    browser.sleep(5000);
  }
  canLogIn() {
    return $('#btnShowLogin').isPresent();
  }
  isLoggedIn() {
    return $('#btnLogout').isPresent();
  }
  logOut() {
    $('#btnLogout').click();
    browser.sleep(1000);
  }
}
Listing 14-6

Page object of login page

With the class LogInPage, the test suite for user management is very simple; see Listing 14-7. When the page is loaded, we check the user can do the login. After we call the method logIn(), we check the user is logged in. The we call the method logOut() and check the user can do the login again.
import { LogInPage } from './login.po';
describe('user', () => {
  it('should be able to log in and log out', () => {
    const loginPage = new LogInPage();
    loginPage.get();
    expect(loginPage.canLogIn()).toBe(true);
    loginPage.logIn();
    expect(loginPage.isLoggedIn()).toBe(true);
    loginPage.logOut();
    expect(loginPage.canLogIn()).toBe(true);
  });
});
Listing 14-7

Test spec of login page

Favorites Page Test

The page object of the favorites page in Listing 14-8 uses the class LogInPage to handle the login. In the method addToFavorite(), we find the first item element with a Like button, which is the item to add to the favorites. We click the Like button to add it and return the item’s title. In the method isInFavorites(), we go to the favorites page and check that the title of the newly liked item is in the array of the titles of all items.
import { browser, by, $, $$ } from 'protractor';
import { LogInPage } from './login.po';
export class FavoritesPage {
  logInPage: LogInPage = new LogInPage();
  get() {
    this.logInPage.get();
    this.logInPage.logIn();
  }
  viewFavorites() {
    $('#btnShowFavorites').click();
    browser.sleep(5000);
  }
  addToFavorite() {
    const itemElem = $$('app-top-stories app-item').filter(elem => {
      return elem.$('.btnLike').isPresent();
    }).first();
    if (itemElem) {
      const title = itemElem.$('h2').getText();
      itemElem.$('.btnLike').click();
      browser.sleep(2000);
      return title;
    }
    return null;
  }
  isInFavorites(title: string) {
    this.viewFavorites();
    expect($$('app-favorites-list h2').map(elem => elem.getText())).toContain(title);
  }
}
Listing 14-8

Page object of the favorites page

Listing 14-9 is the test spec of the favorites page. The test spec checks that items can be added to the favorites.
import { FavoritesPage } from './favorites.po';
describe('favorites page', () => {
  beforeEach(() => {
    this.favoritesPage = new FavoritesPage();
    this.favoritesPage.get();
  });
  it('should add stories to the favorites', () => {
    const title = this.favoritesPage.addToFavorite();
    if (!title) {
      fail('No stories can be added.');
    }
    this.favoritesPage.isInFavorites(title);
  });
});
Listing 14-9

Favorites page test

Build

After we add unit tests and end-to-end tests, we need to introduce the concept of continuous integration that makes sure every commit is tested.

Headless Chrome for Tests

Currently we use Chrome to run unit tests, which is good for local development and debugging, but it makes the continuous integration harder as it requires managing external browser processes. It’s not recommended to use PhantomJS for tests any more. A better choice is to use the headless browser Headless Chrome ( https://developers.google.com/web/updates/2017/04/headless-chrome ).

Switching from Chrome to Headless Chrome is an easy task; we just need to update the settings browsers in karma.conf.js to be ['ChromeHeadless']. When running for continuous integration, we should use the option --single-run to close the browser after the test.

We can configure Headless Chrome to use different options. In Listing 14-10, the browser CustomHeadlessChrome extends from ChromeHeadless with different flags. The flag --no-sandbox is required if Chrome is running as the root user. It’s better to create a new Karma configuration file karma.ci.conf.js to be used when running on continuous servers. In this case, we can add a new script ci:test to run the command ng test --karma-config src/karma.ci.conf.js.
module.exports = function (config) {
  config.set({
    browsers: ['CustomHeadlessChrome'],
    customLaunchers: {
      CustomHeadlessChrome: {
        base: 'ChromeHeadless',
        flags: ['--disable-translate', '--disable-extensions', '--no-sandbox']
      }
    },
  });
};
Listing 14-10

Configure Headless Chrome

For end-to-end tests, we also need to update Protractor to use Headless Chrome. Listing 14-11 shows the Protractor configuration file protractor.ci.conf.js for continuous integration. This file extends from standard protractor.conf.js and overrides the configuration for Chrome. The option --headless makes Chrome running in headless mode.
const parentConfig = require('./protractor.conf').config;
exports.config = Object.assign(parentConfig, {
  capabilities: {
    browserName: 'chrome',
    chromeOptions: {
      args: ['--headless', '--disable-gpu', '--window-size=800x600', '--no-sandbox'],
    }
  }
});
Listing 14-11

protractor.conf.js for CI

To use this Protractor configuration file, we need to create a new script ci:e2e to run the following command.
ng e2e --protractor-config e2e/protractor.ci.conf.js

Gitlab CI

We can integrate the app build process with different continuous integration servers. Here Gitlab CI is used as an example. If you have a local Gitlab installation, you need to install Gitlab Runner ( https://docs.gitlab.com/runner/ ) to run CI jobs.

Gitlab uses Docker to run continuous integrations. We add the file .gitlab-ci.yml in the root directory to enable CI on Gitlab; see Listing 14-12. We use the alexcheng/ionic as the Docker image to run. In the before_script, we use npm to install the dependencies. The first job unit-test runs the unit tests using npm run ci:test; the second job e2e-test runs the end-to-end tests using npm run ci:e2e.
image: alexcheng/ionic:latest
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
before_script:
  - npm i
unit-test:
  script:
    - npm run ci:test
e2e-test:
  script:
    - npm run ci:e2e
Listing 14-12

.gitlab-ci.yml

Summary

In this chapter, we discussed how to use Protractor to test Ionic 4 apps and use Gitlab CI to run continuous integration. Apart from the unit tests we added in the previous chapters, end-to-end tests are also very important in testing Ionic 4 apps to make sure that different components are integrated correctly. Continuous integration is also important for the whole development life cycle that guarantees every code commit is tested. In the next chapter, we’ll discuss some topics about publishing the app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.82.252