Blog post

End-to-end Testing

/partners/client-logo-1.svg
/partners/client-logo-2.svg
/partners/client-logo-3.svg
/partners/client-logo-4.svg
/partners/client-logo-5.svg
/partners/client-logo-6.svg

End-to-end Testing

Nikola Jovanovic

2021-04-14

Introduction

AngularJS official documentation states the following:

As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to verify the correctness of new features, catch bugs and notice regressions. Unit tests are the first line of defense for catching bugs, but sometimes issues come up with integration between components which can't be captured in a unit test. End to end tests are made to find these problems.

This means that E2E testing is about testing our application's interface and functionality. You will better understand its purpose by using it. The framework which we will use to learn about E2E testing will be Cypress.

Cypress

Cypress is one of the best E2E testing frameworks out there. Its learning curve is significantly smaller compared to other testing tools like the ones build upon Selenium. It is packed with great features such as:

  • Debuggability: it allows us to use Developer Tools, which other E2E testing tools don’t let us do;

  • Time Travel: it allows us to traverse through all of the tests that we ran and see how our app looks and what has changed in each of those tests. This is made possible my Cypress' snapshots;

  • Real-time reloads: as we change our tests, Cypress will automatically reload, and we can see the changes we made in real-time;

  • Screenshots and Videos: we can record videos when we run our test suites or automatically take screenshots of failed tests;

  • Speed: since Cypress isn’t Selenium-based, executing your tests will be very fast.

Setup

Cypress can be installed by downloading it from its official website or by using NPM:

1npm init
2npm install cypress --save-dev

Notice that we save it as a development dependency.

After installing, you will have a new folder and a new file in your project folder:

Withing the cypress folder, we have several new folders as well. The most important one for us is the integration folder. All of our tests will be held in this folder. Right now, it contains multiple examples which are downloaded along with Cypress. Feel free to check these examples out yourself.

Now we can run Cypress by typing the following into our command line:

1npx cypress open

A more common way to do this is to create a script in the package.json file:

1{
2  "scripts": {
3    "cypress:open": "cypress open"
4  }
5}

Now we can run it by typing npm run cypress:open.

This will open an Electron app used to run our test in a dedicated browser of your choice. You will see all the available tests:

These tests are used to demonstrate every Cypress has. They will automatically be run in a dedicated browser as soon as you click on one of them. Click on the actions.spec.js test so we can see how it performs. When the tests finish, you will see the following:

Testing

On the right, we can see the website that we are inspecting. We initiate this website with:

1beforeEach(() => {
2  cy.visit('https://example.cypress.io/commands/actions')
3})

We use beforeEach() to run a certain function before every test within our test suit. Test suites are defined with context() and describe(). They are identical so you can use whichever one you like. Test suits are made up of tests. We define tests with specify() and it(). They are also identical.

All of these global functions are provided by Moncha, which is installed along with Cypress. It also comes with the assertion library - Chai.

You can see how these functions are used in the actions.spec.js file:

1context('Actions', () => {
2  beforeEach(() => {
3    cy.visit('https://example.cypress.io/commands/actions')
4  })
5
6  it('.type() - type into a DOM element', () => {
7    cy.get('.action-email')
8      .type('fake@email.com').should('have.value', 'fake@email.com')
9
10      // .type() with special character sequences
11      .type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
12      .type('{del}{selectall}{backspace}')
13
14      // Delay each keypress by 0.1 sec
15      .type('slow.typing@email.com', { delay: 100 })
16      .should('have.value', 'slow.typing@email.com')
17      
18      // ...
19  })
20// ...
21})

context() takes two arguments. The first one is the name of the test suit, and the second one is the function inside which we create our test suit. it() takes similar arguments, first the test description, then the actual test in the form of a function.

We write our test logic within that test using mainly the Cypress API cy, but you can also write Chai assertions within these tests.

Querying

First, we query for an element using cy.get(). Here we pass a selector so we can select a DOM element. This selector can be:

  • DOM element selector: button, div, span

  • ID selector: #

  • Class name selector: .

  • HTML attribute: name, type

  • Data attribute: data-*.

The best selector to use for your elements is the data attribute selector. This is because this selector is the least one likely to change during development. It is common practice data attributes data-cy-*. Here you can read more about selecting elements. You can also use cy.contains() to select your elements using their text content.

Commands

In this example, the class selector selects an element with a class name .action-email. Then, by chaining commands together, we can interact with the selected part. First, we used type() to type an email inside of the selected element. Then we used should() to check if the element has the value of the typed-in text. This can be useful to test controlled components within React. This chain of command is executed until there are no commands left or until it encounters an error. If it encounters a mistake, it will be displayed in the Command Log seen on the left.

Our chains of command can get much more complicated than this by combining more commands which Cypress provides:

  • .blur() - Make a focused DOM element blur,

  • .focus() - Focus on a DOM element,

  • .clear() - Clear the value of an input or textarea,

  • .check() - Check checkbox(es) or radio(s),

  • .uncheck() - Uncheck checkbox(es),

  • .select() - Select an <option> within a <select>,

  • .dblclick() - Double-click a DOM element,

  • .rightclick() - Right-click a DOM element.

All of these functions return Promises. This is why they possess the ability to be used one after another.

Assertions

We mentioned that we use should() to check if, after a specific action, our element contains a particular value. This is called an assertion. You might think that assertions are the only way we use to test our elements, but this isn’t entirely true.

Some of the commands we used to interact with selected elements and the ones we used to select these elements are assertions. For example, cy.get() wouldn’t have selected .action-email if it didn’t exist in the DOM. Another example would be click() because the selected element wouldn’t have been possible to click on if it was cover by another element. So click() has to assert that it isn’t covered, and if it is, it throws an error.

As you can see, there multiple ways our tests can fail without even using assertions. Here are a few more from the official documentation:

  • cy.visit() expects the page to send text/html content with a 200 status code,

  • cy.request() expects the remote server to exist and provide a response,

  • cy.contains() expects the element with content to eventually exist in the DOM,

  • cy.get() expects the element to eventually exist in the DOM,

  • .find() also expects the element to eventually exist in the DOM,

  • .type() expects the element to eventually be in a typeable state,

  • .click() expects the element to eventually be in an actionable state,

  • .its() expects to eventually find a property on the current subject.

Command Log

On the left side of the Cypress test runner is a menu called the Command Log, in which all of the tests written in the actions.spec.js file are displayed. When you click on one of these tests, you will see every execution step that happened during the tests' execution.

You can interact with these steps in different ways. You can hover over them to see what the DOM looked like during that particular step. You can also pin certain steps so that they stay displayed no matter which step you hover over.

On some steps, you can even show how they interacted with a look before and after their interaction.

Conclusion

Official documentation summarizes basic steps of testing your web application as following:

  1. Visit a web page,

  2. Query for an element,

  3. Interact with that element,

  4. Assert about the content on the page.

Once you start applying these steps to your projects, testing will become easy and a hell of a lot more fun compared to writing unit tests.

Once you get the basics of Cypress down, you can continue learning from their official documentation, which is one of the best documentation I’ve read in a while. This technology is relatively new, so there might not be many other resources to learn from anyway.

Compared to other E2E testing tools out there, Cypress is the easiest to learn and one of the fastest. I highly recommend learning more about it if you’re interested.

Nikola Jovanovic

2021-04-14

Nikola is software engineer with a problem solving mindset, in love with JavaScript and the whole ecosystem. Passionate about frontend and backend work.

See more blogs:

Leave your thought here

Your email address will not be published. Required fields are marked *