Protractor is dead, long live Cypress! - Part 2
Tags: angular, protractor, e2e, cypress
Published: 2021-07-05 07:21:31

## Table of Contents
  * [Basics](#basics)
  * [First Test](#first-test)
  * [Look out for Flakiness](#flakiness)
  * [Be careful with Asynchronity](#asynchronity)
  * [Further Reading](#further-reading)

On 24th April, Angular announced the deprecation of their end-to-end (E2E) testing tool, Protractor. It remains unclear whether there will be a successor or if Angular will delegate this to its users. At the time of this writing, WebDriver.IO, TestCafé and Cypress have come up with schematics for the Angular CLI.

This is the follow-up to my article about E2E frameworks in general in which I will help you get started with E2E testing in Cypress.

You can find  the sources files on 
{% github rainerhahnekamp/angular-cypress no-readme %}

If you prefer watching over reading, then this recording of my talk is for you:
{% youtube -lWY0X-ybME %}

## Basics <a name="basics"></a>
Cypress is extremely easy to use. Starting from Angular 12, you just need to run the schematics  like `npx ng add @cypress/schematic` and voilá, done. If you are on nx, which I recommend, Cypress is already pre-installed.

Cypress tests are written like most of the other tests in JavaScript. describe defines a new test suite and contains multiple test cases, where each one is defined by it. They are located in the folder **/cypress/integration**.

E2E tests do the same things a human tester would do. They are looking, clicking and typing. Each of these three actions has its own command in Cypress which is actually a method of the global cy object. These methods can be chained to create complex test paths.

Before we can do something with a DOM node, we have to look it up first. This is done via `cy.get("some-selector")`. Then we can run an action on it such as `click()` or `type("some text")`. A click on a button is `cy.get('button').click()`. Isn't that easy?

Since we write a test we want to verify that something has happened after the click. We expect a text message to appear in a paragraph within the selector `p.message`. It should show "Changes have been saved". We would assert it like that: `cy.get('p.message').should('contain.text', 'Changes have been saved');`.

## The First Test <a name="first-test"></a>

Let's just write the test we described above.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brhekr2ig8bgqs4e9wdn.png" alt="First Test"><figcaption>First Test</figcaption>
</figure>

Given the knowledge we have so far, we can do that in no time. We create the test file in **/cypress/integration/home.spec.ts** and write following code:

```javascript
describe("Home", () => {
  it("should click the button", () => {
    cy.visit("");
    cy.get("button").click();
    cy.get("div.message").should("contain.text", "You clicked me");
  })
})
```


So how do we run it? Again, very easy. Make sure that the Angular application itself is also running and just execute `npx cypress open` or `npm run cypress:open` to open Cypress. When you click on `home.spec.ts`, the test runner opens in another window and immediately runs the test.


<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irufsdlyxtyo6dmz1npr.png" alt="Cypress UI"><figcaption>Cypress UI</figcaption>
</figure>

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2r4myudpl5f84bq6uxo.png" alt="Cypress Test Runner"><figcaption>Cypress Test Runner</figcaption>
</figure>
 
Did it work? Wonderful! Now what do we have to do when a test should run in a pipeline of our CI? Instead of `npm run cypress:open`, we just execute `npm run cypress:run`. This runs the test in the headless mode. 

Since we can't really see anything, Cypress automatically records the tests and stores the video files in **/cypress/videos**. Additionally, the failed tests will also be screenshotted under **/cypress/screenshots**.

## Look out for Flakiness <a name="flakiness"></a>

Let's say we want to add a customer in our test. In the sidebar we click on the button "Customers", after that the customer list appears along the button "Add Customer". We click on that as well:

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6n4ypvxen8nhtg3nhxg.png" alt="Flaky Test"><figcaption>Flaky Test</figcaption>
</figure>

A test for that can look like:

```javascript
it("should add a customer", () => {
  cy.visit(""); 
  cy.get("a").contains("Customers").click(); 
  cy.get("a").contains("Add Customer").click(); 
})
```

If you run that test, it will probably fail in a very strange way:

![Flaky Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9824cku3do0px1pkmux8.png)

It looks like Cypress cannot find the link with the "Add Customer" even though the button is right in front of it. What's going on there?

The answer is quite clear. We might think that `cy.get("a")contains("Add Customer")` is continuing to look for a link with the text "Add Customer" for a maximum of 4 seconds. That is not true.

What we see here are two commands which run sequentially. The first command is the lookup for all link tags. If Cypress finds some, it applies the next command on those. In our case, the "Add Customer" link does not immediately render after the click on "Customers". When Cypress looks for links, it only finds two: the "Customers" and the logo in the header. It then waits for the text in one of these two links to become "Add Customer".

In some cases, the rendering of "Add Customer" is fast enough and Cypress will find 3 links and succeed. In other cases, it won’t. So we end up having tests that sometimes fail and sometimes succeed. A nightmare!

Always remember these two rules:

1. Commands are not retried when they are successful
2. Chains are multiple commands

So how to avoid it? We should come up with better selectors that avoid splitting the selection process into two commands. I prefer to apply `data-test` with a unique identifier to my DOM elements. The markup for the two links would look like this:

```html
<a data-test="btn-customers" mat-raised-button routerLink="/customer">Customers</a>
<a [routerLink]="['.', 'new']" color="primary" data-test="btn-customers-add"
mat-raised-button
>Add Customer</a>
```
 
We end up with following rewritten test:

```typescript
it("should click on add customers", () => {
  cy.visit("");
  cy.get("[data-test=btn-customers]").click();
  cy.get("[data-test=btn-customers-add]").click();
})
```

## Be careful with Asynchronity <a name="asynchronity"></a>

Cypress commands like `cy.get` have an awaiting feature built in. This means they will keep trying multiple times until an action is doable or the element is found. That constant retrying happens asynchronously. You could read the test case like this:

```typescript
it('should click on add customers', () => {
  cy.visit('')
    .then(() => cy.get('[data-test=btn-customers]'))
    .then((button) => button.click())
    .then(() => cy.get('[data-test=btn-customers-add]'))
    .then((button) => button.click());
});

it('should click on add customers', async () => {
  await cy.visit('');
  const button = await cy.get('[data-test=btn-customers]');
  await button.click();
  const button2 = await cy.get('[data-test=btn-customers-add]');
  await button2.click();
});
```

Although these commands provide a then method, don't mistake them for Promises. And yes, you must not write code like shown above. Cypress queues and runs the commands internally. You have to be aware of its "internal asynchronicity" and avoid mixing it with synchronous code like that:

```typescript
it('should fail', () => {
  let isSuccessful = false;
  cy.visit('');
  cy.get('button').click();
  cy.get('div.message').then(() => {
    isSuccessful = true;
  });

  if (!isSuccessful) {
    throw new Error('something is not working');
  }
});
```

After running the test, we get the following result:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8fjspjdecxdqx7mudaf.png)

What happened there? It looks like the application did not even open! That's right. Cypress just queued all cy commands to run them asynchronously but the let and the condition with the throw command are synchronous commands. So the test did fail before Cypress had a chance to run the asynchronous parts. Be aware of that. You can run synchronous code only in the `then` methods.

And this closes our quick introduction to Cypress. As next steps I recommend that you switch to [Cypress.io](https://www.cypress.io/). The official documentation is superb.

And last but not least, allow me some shameless advertising from my side 😅. [AngularArchitects.io](https://www.angulararchitects.io/en/angular-workshops/5094/) provides a 3-day training about testing for Angular developers. It also includes Cypress and is held as public training but can also be booked in-house.

# Further Reading <a name="further-reading"></a>

- [Migrating from Protractor to Cypress](https://docs.cypress.io/guides/migrating-to-cypress/protractor)
- [@cypress/schematic](https://www.npmjs.com/package/@cypress/schematic)