An Alternative to Protractor for Angular Projects

December 11, 2017

By Gleb Bahmutov

Fans of Angular CLI get Protractor end-to-end tests generated with each scaffolded project. In this blog post I will show how to add Cypress E2E tests instead with minimum effort (and TypeScript support)!

Scaffolding a project

Scaffolding an Angular project using ng new <name> is a huge time saver. Just 4 commands give you a complete “Hello Angular” application with all recommended practices. Let us make an app - you can find the results in bahmutov/ng-cli-hello repository.

npm install -g @angular/cli
ng new ng-cli-hello
cd ng-cli-hello
ng serve
Open http://localhost:4200/ in the browser to see the running application - it even supports hot module reload!

Our application has a greeting, Angular logo image and a few links.

Protractor tests

End-to-end tests are automatically scaffolded using Protractor. When we execute ng e2e command, Angular CLI takes care of all installation details, then runs the tests in the real browser (Chrome).

$ npm run e2e

> [email protected] e2e /ng-cli-hello
> ng e2e

** NG Live Development Server is listening on localhost:49152, open your browser on http://localhost:49152/ **
...

webpack: Compiled successfully.
[21:31:38] I/file_manager - creating folder /ng-cli-hello/node_modules/protractor/node_modules/webdriver-manager/selenium
[21:31:40] I/update - chromedriver: unzipping chromedriver_2.33.zip
[21:31:40] I/update - chromedriver: setting permissions to 0755 for /ng-cli-hello/node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.33
[21:31:40] I/launcher - Running 1 instances of WebDriver
[21:31:40] I/direct - Using ChromeDriver directly...
Jasmine started

  ng-cli-hello App
    ✓ should display welcome message

Executed 1 of 1 spec SUCCESS in 0.993 sec.
[21:31:43] I/launcher - 0 instance(s) of WebDriver still running
[21:31:43] I/launcher - chrome #01 passed

The single test in the file e2e/app.e2e-spec.ts confirms the greeting text.

import { AppPage } from './app.po';

describe('ng-cli-hello App', () => {
  let page: AppPage;

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

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toEqual('Welcome to app!');
  });
});

Note the use of a page object - which is defined in e2e/app.po.ts. The page object abstracts common interactions with the application like opening the page or getting the greeting text.

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

export class AppPage {
  navigateTo() {
    return browser.get('/');
  }

  getParagraphText() {
    return element(by.css('app-root h1')).getText();
  }
}

Cypress tests

Recently, our team at Cypress has added TypeScript support to our Test Runner via plugins and preprocessors. We have even released webpack and browserify TypeScript recipes, further simplifying the user workflow. Let us setup Cypress tests in less than 60 seconds.

npm i -D cypress
$(npm bin)/cypress open

Installing the Cypress NPM module should take just a few seconds on Mac, Linux or Windows platforms - it downloads an Electron-based binary for the current OS. When running Cypress for the very first time, it scaffolds e2e tests in a cypress folder.

We are not interested in the default example tests, so let’s go ahead and delete example_spec.js and create an empty spec.ts instead.

rm cypress/integration/example_spec.js
git touch cypress/integration/spec.ts

By default Cypress understands JavaScript and CoffeeScript source files. I want to write TypeScript, not copy and paste the example code and configure Cypress plugins. So I wrote NPM module @bahmutov/add-typescript-to-cypress that does the Cypress preprocessor configuration using a post-install hook.

npm i -D @bahmutov/add-typescript-to-cypress
+ @bahmutov/[email protected]

During installation, it has written cypress/plugins/index.js to load TypeScript files using ts-loader (via @cypress/webpack-preprocessor).

const wp = require('@cypress/webpack-preprocessor')

const webpackOptions = {
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      }
    ]
  }
}

module.exports = on => {
  const options = {
    webpackOptions
  }
  on('file:preprocessor', wp(options))
}

Now we can start writing tests. Open Cypress again, click on spec.ts in the list of tests and let it watch the tests in the spec file. First, let us confirm that the page loads. Note, during tests I have ng serve still running - the application is available at localhost:4200.

// cypress/integration/spec.ts
it('loads', () => {
  cy.visit('http://localhost:4200');
});

note: if your editor’s IntelliSense (I am using VS Code) does not pick up the definition for the cy or Cypress objects automatically, add to the root tsconfig.json file "include": ["node_modules/cypress"] folder. Or create tsconfig.json right in the cypress folder to have local TypeScript settings for the end-to-end tests. Here is my tsconfig.json file.

{
  "extends": "../tsconfig.json",
  "include": [
    "integration/*.ts",
    "support/*.ts",
    "../node_modules/cypress"
  ]
}

With TypeScript definitions found, our editor can show helpful information as we type Cypress commands, or when we hover over the code.

If you find an error in our typings, please take a minute to fix them in the index.d.ts file and open a pull request.

Even this simple test above is useful:

  • It runs in a real browser: Electron, Chrome (support for other browsers is coming)
  • It only passes if the server returns an HTML page with successful HTTP code

Let us confirm the greeting text.

it('loads', () => {
  cy.visit('http://localhost:4200');
  cy.get('app-root h1').contains('Welcome to app!');
});

Here is where Cypress really shines - it is  developer focused tool. It shows every step of the test in its Command Log on the left, and if I hover over a step, like “CONTAINS” the test area highlights the current element being tested.

The Protractor tests use a page object to abstract the implementation (like the exact page selectors) from the test. We can abstract interaction with the page the same way by moving utility functions into the cypress/support folder.

I am a strong believer in utility functions rather than page objects - I think using functions simplifies code a lot.

// cypress/support/po.ts
// we could place this url into cypress.json as "baseUrl"
const url = 'http://localhost:4200';

export const navigateTo = () => cy.visit(url);

export const getGreeting = () => cy.get('app-root h1');

Let us rewrite the greeting test using these functions.

// cypress/integration/spec.ts
import { navigateTo, getGreeting } from '../support/po';

describe('Hello Angular', () => {
  beforeEach(navigateTo);

  it('should display welcome message', () => {
    getGreeting().contains('Welcome to app!');
  });
});

Let us add another test - to confirm that we have 3 links on the page.

it('has 3 links', () => {
  cy.get('app-root li a').should('have.length', 3);
});

Great! The Angular project now can take full advantage of Cypress’ extensive command API, video recording, screenshots on failure, CI support, and many other developer-friendly features.

If you like Cypress, star it on GitHub or tweet about it. If you feel Protractor is still a better tool for end-to-end testing - tell us why.