Learn Great E2E testing with Cypress - you will love writing tests after this
Tags: tutorial, webdev, showdev, javascript
Published: 2019-11-21 23:36:15

---
title: Learn Great E2E testing with Cypress - you will love writing tests after this
published: true
description: This article will describe how to use the very competent test runner Cypress. It showcases some of Cypress's greatest features
tags: tutorial, webdev, showdev, javascript
cover_image: https://thepracticaldev.s3.amazonaws.com/i/gvef5c5trkkaf09me6h0.jpg
canonical_url: https://softchris.github.io/pages/cypress.html
---

Follow me on [Twitter](https://twitter.com/chris_noring), happy to take your suggestions on topics or improvements /Chris

> Testing is a necessary thing, yet are tools we use for testing aren't great which leads to us both authoring and running tests less than we should. Why can't we have a great tool for testing? Well we can - with Cypress

TLDR; The test tool of the future is here. It sounds like a dream, read on and I'm sure by the end of the article you will agree with me. Cypress - a test runner built for humans.

## References

- [How Cypress controls a Browser](https://docs.cypress.io/guides/guides/launching-browsers.html)
If you are interested in knowing more how Cypress deals with Browsers than this is a good page
- [How Cypress works](https://www.cypress.io/how-it-works)
Great doc page that explains how Cypress works under the hood
- [Angular + Cypress repo](https://github.com/softchris/cypress-demo)
You can easily take the Cypress tests and build a similar app in Vue or React, that's the beauty of Cypress.
- [Installing Cypress](https://docs.cypress.io/guides/getting-started/installing-cypress.html)
- [Writing your first test with Cypress](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html)
- [Testing strategies](https://docs.cypress.io/guides/guides/network-requests.html#Testing-Strategies
https://docs.cypress.io/api/api/table-of-contents.html)
It's always good to think of the approach to use when testing


## WHAT

Cypress is a testing tool that greatly improves your testing experience. It offers features such as:
- **Time travel**, it takes snapshots of your tests as you run the tests. This enables you to easily hover over each command that took place in your test
- **Debuggability**, Debug directly from familiar tools like Chrome DevTools. Our readable errors and stack traces make debugging lightning fast
- **Real-time reloads**, Cypress automatically reloads whenever you make changes to your tests. See commands execute in real-time in your app.
- **Automatic waiting**, Never add waits or sleeps to your tests. Cypress automatically waits for commands and assertions before moving on - *No more async hell*.
- **Spies, stubs, and clocks**, Verify and control the behavior of functions, server responses, or timers. The same functionality you love from unit testing is right at your fingertips.
- **Consistent results**, Our architecture doesn’t use Selenium or WebDriver. Say hello to fast, consistent and reliable tests that are flake-free.
- **Network traffic control**, Easily control, stub, and test edge cases without involving your server. You can stub network traffic however you like.
- **Screenshots and videos**, View screenshots taken automatically on failure, or videos of your entire test suite when running headlessly.



## WHY

Testing today doesn't feel like a first-class citizen. We often have a great looking IDE for writing code but authoring and running tests feels lacking - like an afterthought. We need testing to feel confident in what we are building but we should have a right to expect first-class tools. 

One problem with E2E tests is that they are flaky. Cypress offers a great test runner, that's not flaky, for running Unit as well as E2E tests, the future is here.

## Installing Cypress

You want to install Cypress as a dependency to your project. Make sure `npm init` has been run. I usually go with the *smart defaults* option:

```
npm init -y
```

Then install Cypress with:

```
npm install cypress
```

It's possibly to use Yarn as well:

```
yarn add cypress --dev
```

Your executable now exist on:

```
./node_modules/.bin/
```

## Starting Cypress

You want to be calling `cypress open` to start Cypress. You can do so in one of the following ways:

1. `./node_modules/.bin/cypress open`
2. `$(npm bin)/cypress open`
3. `npx cypress open` for NPM version > 5.4, or `npx` installed separately
4. `yarn run cypress open`

We'll go with `npx cypress open`:

![](https://thepracticaldev.s3.amazonaws.com/i/bvti2o2xxpcay71vemx3.png)

This also pops up a window, looking like this:

![](https://thepracticaldev.s3.amazonaws.com/i/ypopdypilnfc8u0zqsue.png)

According to the header text it has added test samples. Let's have a look at our project structure

![](https://thepracticaldev.s3.amazonaws.com/i/nk2j8y7v59poet5v58y7.png)

Above we see that we got a directory `cypress` containing an `integration` subfolder, like so:

```
-| cypress/
---| integration
------| examples
```

Looking at the content of the `examples` folder we can see that it contains everything we could possibly want to know how to do like:
- writing tests
- mock APIs
- different assertions
- aliases 

and much much more. This is great, we will have a lot of reasons to return back, but first we need to learn how to crawl. So where to start?

## Our first test

Let's create a new file `sample_spec.js` under the `cypress/integration` folder:

```
- | cypress
---| integration
------| sample_spec.js
```

Upon creating the file, it gets picked up by our test runner, that's still running:

![](https://thepracticaldev.s3.amazonaws.com/i/bbqvixvt1dcjrwtfhdte.png)

Let's add the following content to `sample_spec.js`:

```js
describe('My First Test', function() {
  it('Does not do much!', function() {
    expect(true).to.equal(true)
  })
})
```

Let's save the content and let's click the test in our test runner. Doing so should produce the following window:

![](https://thepracticaldev.s3.amazonaws.com/i/3h9e3wv0vjzndvd2h0q7.png)

We have a passing test :)

We can easily make it fail by changing the code:

```js
describe('My First Test', function() {
  it('Does not do much!', function() {
    expect(true).to.equal(false)
  })
})
```

As soon as we save the code - the runner now displays this:

![](https://thepracticaldev.s3.amazonaws.com/i/v51ry2669ko9d4hdwkg9.png)

## A real test

Let's write something more real looking. So far we tested `true` vs `true`. While that technically is a *a test* - we wanted to show what Cypress really can do. So let's test out its capability on a web page. 

Our high-level approach looks like so:

1. **Visit** a web page.
2. **Query** for an element.
3. **Interact** with that element.
4. **Assert** about the content on the page.

### Visit

Cypress has provided as with numerous helpers to make the above really easy to achieve. First, let's set up a test suite with a test in it. Let's create a file `page_spec.js` under our `integration` folder

```
-| cypress/
---| integration/
------| page_spec.js
```

Now, give it the below content:

```js
describe('My First Test', function() {
  it('Visits page', function() {
    cy.visit('https://example.cypress.io')
  })
})
```

We can see that we use the global object `cy` and the helper method `visit()` to go to a page. 

Per usual our `Visits page` test shows up and we are able to click it. Now we are faced with the following UI:

![](https://thepracticaldev.s3.amazonaws.com/i/gh608lx54t1scsjehftk.png)

On our left, we see our test suite, test and what action we are currently on aka `VISIT` and to our right we see the result of carrying out said action, which is the webpage we navigated to.

### Query for an element

Now let's find an element. There are many helpers that help you find an element but let's find this one by content:

```
cy.contains('type')
```

Let's add this to our code so our test now reads:

```js
describe('My First Test', function() {
  it('Visits page', function() {
    cy.visit('https://example.cypress.io')
    cy.contains('type')
  })
})
```

Let's save this. Note how our test runner now says:

![](https://thepracticaldev.s3.amazonaws.com/i/4em1jilqdcl6tgry11xb.png)

Above we see our `cy.contains()` created a `CONTAINS` action on our left. On our right, we see how the element is *highlighted* that matches our `cy.contains()`. 

### Interact

Next step is to interact with our UI and specifically with our found element, so let's click it, like so:

```js
cy.contains('type').click()
```

Let's save this. You should have the below result:

![](https://thepracticaldev.s3.amazonaws.com/i/v8xgdtz25pccpzl68in5.png)

Clicking our `type` element expanded it and it's now showing us a lot of content that we can assert on. 

### Assert

Ok then. There are more than one thing we could assert on here:

- **URL**, our URL actually changed from us clicking this element
- **Content**, new content is being shown, let's assert it's the correct content

To assert on the URL we can use the helper `cy.url().should('include', '<something>')`. We are using multiple helpers here:

- `cy.url()`, this helps us grab the URL
- `.should()`, this is an assertion
- `include`, this is a keyword telling us what part of the URL should be matched against our desired output

In our case, we want the expression to read:

```
cy.url().should('include', '/commands/actions')
```

which means that we want the URL to *contain* `/commands/actions`

What about other types of assertions, like input elements? For that, we can use the `cy.get()` helper, like so:

```
cy.get('.action-email')
    .should('have.value', '<some value>')
```

Above we are getting the email by CSS class.

Let's add the above assertions to our test so the test now reads:

```js
describe('page test - suite', () => {
  it('Visits page', function () {
    cy.visit('https://example.cypress.io')
    cy.contains('type').click()
    cy.url().should('include', '/commands/actions')

    cy.get('.action-email')
      .should('have.value', '')
  })
})
```

Let's save this new code. You should get the below update in the UI as the test is rerun.

![](https://thepracticaldev.s3.amazonaws.com/i/h3ylyghm4nxtate2prwy.png)

As we can see it is able to correctly assert on the URL as well as the element. 

Let's try to change the content of the input element though, just to ensure it gets properly updated (Yes I have messed that up in every single SPA framework I've coded with :) ).

To enter content into an input element using the helper `.type()`, like so:

```js
cy.get('.action-email')
    .type('fake@email.com')
    .should('have.value', 'fake@email.com')
```

Saving this, and having the test re-run, results in the following:

![](https://thepracticaldev.s3.amazonaws.com/i/o9hhetuq53l6bdpzz078.png)

We see above that it types into our text element, so yes we didn't mess up the 2-way, unidirectional data-flow that we were using with our SPA (our we could have been using Vanilla JS ;) )

## Debugging

Ok, so we've learned a bit above on how to author and run our tests and everything went mostly green. What if it doesn't though, what if we have problems? For that, we can use the excellent debug support in the form of Time traveling and Snapshots. For every single action carried out you can freely go back and forth between snapshots. Let's demonstrate this

![](https://thepracticaldev.s3.amazonaws.com/i/y2b7iq93toexnc89rhnp.gif)

Additionally to our Snapshots, we can use two additional commands:

- `cy.pause()`, this gives us the ability to pause on a specific place in our test
- `cy.debug()`

### cy.pause()

  By adding this in the code like so, for example:

```js
describe('page test - suite', () => {
  it('Visits page', function () {
    cy.visit('https://example.cypress.io')
    cy.contains('type').click()
    cy.url().should('include', '/commands/actions')

    cy.pause()

    cy.get('.action-email')
      .should('have.value', '')

      cy.get('.action-email')
        .type('fake@email.com')
        .should('have.value', 'fake@email.com')
  })
})
```

We code the test execution to halt, like below:

![](https://thepracticaldev.s3.amazonaws.com/i/pzsstx4a0jpm42e6t4l6.png)

As you can see from the above picture, we also get a control panel that indicates what state we are in *paused*, a *play* button to resume the test run and also a `step` button to the right that allows us to step each line of code in our test.

### cy.debug()

What if we replace our `cy.pause()` for `cy.debug()`? Well, that works a bit differently. If you pull up developer tools the code will halt on `debugger` line like so:

![](https://thepracticaldev.s3.amazonaws.com/i/l0u6g2atbz11gn3g2xrm.png)

If you go to the console you get some additional logging, like so:

![](https://thepracticaldev.s3.amazonaws.com/i/vu0djparg077jmsy2gi5.png)

So this is an additional way of getting Developer Tools to help out debugging whatever your issue is. 

## Mocking

*Mocking* is an interesting topic. It's interesting because this is about what kind of tests we want to write. If we Mock the backend do we actually have a true E2E test? No we don't but there is still value to mock certain endpoints of your entire backend just to make sure we can easily test certain scenarios. So let's talk about how we can achieve that.

There are two ways we could be mocking:

1. **Inline code Mocking**, this means we will intercept a certain route call and answer with a javascript object
2. **Fixtures**, this is essentially the same as above but instead of having the response as JavaScript code we point to a JSON file

### Inline code Mocking

First we need to call `cy.server()`, this will tell Cypress we allow mocking to happen. Next, we tell it what endpoint to mock, like so:

```js
cy.route({
  method: 'GET',
  url: 'http://localhost:3000/products', 
  response: [{
    id: 1,
    title: 'Mocked Tomato'
  }]
})
```

The above is quite readable but let's explain it anyway:

- **method**, this is the HTTP method we mean to listen to
- **url**, this is simply the URL, we can match it exactly like above or use a more wildcard matching with `*`
- **response**, this is where we specify the response we want instead of letting the actual API call go through

> This doesn't feel quite nice though. Sure, it works, but it feels like we can do this better. We can, with fixtures

### Fixtures

There are a few things we need to know about this one:

- fixtures are JSON files
- Cypress automatically looks in the `fixtures` directory for fixtures
- You can create whatever subdirectories you need and Cypress can find them

Let's show how it can look. Given the following directory structure:

```
-| cypress/
---| fixtures/
------| heroes/
---------| list.json
```

and the following content of `list.json`:

```json
[{
  "id" : 1,
  "title" : "Sir Mockalot"
}]
```

we can now instruct cypress to use the above JSON file like so:

```js
cy.fixture('heroes/list.json').as('heroesList')
cy.route('GET', 'http://localhost:3000/products', '@heroesList');
```

the call to `cy.fixture()` says where my JSON file is relative to `/fixtures` and we und by creating an *alias* `heroesList` that we can use in the next line. The call to `cy.route()` does the same as before but we have to type less. It first takes an HTTP verb, followed by what URL to mock and lastly it takes our *alias*. Note, how we prefix the alias with `@`.

> Just imagine the possibilities being able to specify all the possible scenarios you might need. 

The million-dollar question is where to place our code? Well, the answer is where it's needed. It should before the endpoint is being called. So let's say the above endpoint is being hit when loading the page then the following code would be correct:

```js
cy.fixture('heroes/list.json').as('heroesList')
cy.route('GET', 'http://localhost:3000/products', '@heroesList');

cy.visit('http://localhost:4200')
cy.server()
```

## Adding Cypress to your SPA app

Now, its quite easy to add Cypress to any SPA app out there, it's that good. Let's use Angular as an example, but feel free to apply it to React, Vue or Svelte. We will do the following:

1. **Scaffold** an Angular project
2. **Install** Cypress
3. **Set up** package.json
4. **Write** some tests
5. **Startup** everything


### Scaffold our app

In Angular this is as easy as calling:

```
ng new <name of my project>
cd <name of my project>
```

### Install Cypress

Installing Cypress is accomplished with the following command:

```
npm install cypress --save-dev
```

### Set up package.json

We want to be able to start up our app and Cypress at the same time. There are many ways to do this but a popular option is using the library `concurrently`, which we can install with:

```
npm install concurrently
```

Let's now set up a task in `package.json` and our `scripts` section, like so:

```json
"cypress": "concurrently \"ng serve\" \"cypress open\" \"json-server --watch db.json\""
```

above you can see how we use `concurrently` to start the angular app with `ng server`, followed by starting Cypress with `cypress open` and lastly starting up our API with `json-server --watch db.json`. Now, if you have a real API use whatever command you need to start that up instead. We just use `json-server` as a simple way to pretend we have a real API.

### Write some tests

Ok, let's write some tests given the following app:

![](https://thepracticaldev.s3.amazonaws.com/i/h1ipvi53qdc7bg86bxng.png)

This is a simple *todo* app, we can:

- **Add** items
- **Update** Item
- **Delete** item

**Add item to list**

For this test, we enter a value in a textbox. Then we click a button to add the item and lastly, we assert that the textbox we used for input has been cleared and that the added item exists in the list.

```js
it('should add Hulk to list', () => {
  cy.visit("http://localhost:4200")
  cy.get(".new-hero")
    .type("Hulk")
    .should("have.value", "Hulk")

  cy.get(".new-hero-add")
    .click()
  
  cy.get(".new-hero")
    .should("have.value", "");

  cy.get(".hero-input")
    .eq(2)
    .should("have.value", "Hulk");  
})
```

**Assert we have 2 items in a list**

This just ensures we have 2 items in a list. We grab a reference to the list element and checks that its length is `2`.

```js
it('should have list with length 2', () => {
  cy.visit('http://localhost:4200')
  cy.get(".hero-input")
    .its('length')
    .should("eq", 2);
})
```

**Update item in list**

Here we change an item in the list, then we click to update the item and lastly we assert that the item has been updated.

```js
it('should update our item' , () => {
  cy.visit("http://localhost:4200")
  cy.get(".hero-input")
    .eq(1)
    .should("have.value", "Captain Marvel")
    .type("s")
    ;

  cy.get('.hero-update')
    .eq(1)
    .click()

  cy.get(".hero-input")
    .eq(1)
    .should("have.value", "Captain Marvels");
})
```

**Delete item**

This is about locating an item at a specific index in the list. Then we click the corresponding delete button. Lastly, we assert and ensure that our item is removed from the list.

```js
it('should remove our item', () => {
  cy.visit("http://localhost:4200");
  cy.get(".hero-remove")
    .eq(1)
    .click();

  cy.get(".hero-input")
    .its("length")
    .should("eq", 1);
})
```



### Start up everything

To start everything up we call:

```
npm run cypress
```

To see the full source code of the Angular project with accompanying tests have a look at the following repo.

> https://github.com/softchris/cypress-demo

## TypeScript

Most of the major SPA frameworks today support Typescript. Typescript doesn't always make sense for your project and it's up to you if you want to add it. Remember, you can gradually add parts of it where it makes sense. 

> So how easy is it to add TypeScript to my Cypress tests?

Very easy, the reason for that is that Cypress ships with TypeScript types. The only thing you need is a `tsconfig.json` file with the following content:

```json
{
  "compilerOptions": {
    "strict": true,
    "baseUrl": "../node_modules",
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress"]
  },
  "include": [
    "**/*.ts"
  ]
}
```

With that in place auto-complete will work fine like so:

![](https://thepracticaldev.s3.amazonaws.com/i/tpdx9ewubzm8k1bp4vyc.png)

## Screenshots

Finally, let's talk about something really amazing, namely screenshots, that you get for free. All you need is a call to `cy.screenshot()`. If you place in a lifecycle method like `beforeEach()` it will produce a screenshot for each test in that test suite. 

Screenshots are places in the `screenshots` directory by default. 

Below we have an example where we have invoked the following in `list_spec.js`:

```js
describe('', () => {
  beforeEach(() => {
    cy.screenshot();
  })
})
```

![](https://thepracticaldev.s3.amazonaws.com/i/o0604wl3p6ngv1mbfvu7.png)

## Summary

This has been a somewhat long article but hopefully, you've seen what Cypress can do. To be honest I've only shown you a small fraction. You as a developer, deserve not only a great IDE to code in but also an outstanding test runner. I promise you, give Cypress a chance and next you will volunteer to write tests.