by Francesco Agnoletto

Write your first end-to-end test in 5 minutes

Easy End-to-end testing for any web application

Cypress.io official logo

There are some features and web applications that are not easy to test. Unit tests can only go so far in testing what the end-user sees when visiting your application.

Anything that requires real interaction with the DOM, for example tracking the mouse position or drag and drop, can be easily tested with end-to-end tests.

The main advantage of end-to-end tests is that you write tests that run in a real browser. That’s the closest you can get to the end-user, making these tests highly reliable.
They are also technology agnostic, so whatever framework you happen to use, testing is exactly the same.

Setting up the repository

I’m using this repository as an example since it provides a perfect target to end-to-end tests. It does use react and TypeScript, but don’t worry if you are not familiar with them, we are not going to touch the code. Make sure to delete the /cypress folder as it contains what we are going to do below.

Run npm i and npm start to see how the app works (start is also required to run the tests, so keep it running!).

Setting up cypress

I’m going to use cypress.io as end-to-end testing library. The excellent documentation and ease of installation make it an easy choice to quickly write down some tests.

Since cypress doesn’t require any configuration outside of its own folder, it’s safe to install in any frontend codebase.

$ npm i -D cypress

There are 2 ways of running cypress: in the browser (in watch mode) or the command line (no watch mode). We can try both by adding the scripts in package.json.

{
  "scripts": {
    "cypress": "cypress run", // cmd
    "cypress--open": "cypress open" // browser
  }
}

Running once npm run cypress-open will set up all the files required inside the /cypress folder.

The last step is to set up our port in cypress.json.

{
  "baseUrl": "http://localhost:8080" // change to whatever port you need
}

Writing the first test

Once you ran the command above once, you will have completed the setup to install cypress.
Cypress comes with a few example tests in /cypress/integration/examples but we can delete those, we will write our own soon.

Feel free to visit the application at localhost:8080 to see how it works. The app just handles some mouse inputs. On the main div, pressing the left-click will generate a note, left-clicking the note’s icon will make it editable, and right-clicking on it will delete it.

Create a homepage.spec.js file inside /cypress/integration, and let’s tell cypress to visit our app.

// cypress/integration/homepage.spec.js

it("successfully loads", () => {
  cy.visit("/");
});

And voilà the first test is done! You can check that it passes by running npm run cypress or npm run cypress--open.

Cypress looks very similar to many unit testing libraries like jest or mocha. It also comes with its own assertion library, so it is fully independent.

While this was easy, this test only checks that the app running.

Writing all the other tests

The first interaction in our app regards left-clicking on the main div, so let’s write a test for that.

it("click generates a note in the defined position", () => {
  // First, we check that our base div is indeed empty,
  // no note elements are present in the page
  cy.get("#app > div").children().should("have.length", 0);

  // Cypress provides a very intuitive api for mouse actions
  const pos = 100;
  cy.get("#app > div").click({ x: pos, y: pos });

  // now that we have clicked the div
  // we can check that a note appeared on top of our div
  cy.get("#app > div").children().should("have.length", 1);
});

This test already does enough to make us happy. When the click happens, a new note element is created.
We can improve it further by checking the position of the new note.

it("click generates a note in the defined position", () => {
  // First, we check that our base div is indeed empty,
  // no note elements are present in the page
  cy.get("#app > div").children().should("have.length", 0);

  // Cypress provides a very intuitive api for mouse actions
  const pos = 100;
  cy.get("#app > div").click({ x: pos, y: pos });

  // now that we have clicked the div
  // we can check that a note appeared on top of our div
  cy.get("#app > div").children().should("have.length", 1);


  // Checking the position on the div of our new note
  cy.get("#app > div button")
    .should("have.css", "top")
    // we detract half the size of the button on note.tsx
    // 100 - 12 padding = 88
    .and("match", /88/);

  cy.get("#app > div button")
    .should("have.css", "left")
    // we detract half the size of the button on note.tsx
    // 100 - 12 padding = 88
    .and("match", /88/);
});

An important note on cypress tests, the DOM will not reset between tests, this makes it easy to test incremental features.
We can use this to keep testing the note we created in the previous test. The next interaction we can test is editing.

it("left click on note edits the note content", () => {
  // We don't care for position of the click
  // as long as the click happens inside the note
  cy.get("#app > div button").click();

  // Typing does not happen instantly, but one key at a time
  cy.get("input").type("Hello World");
  // {enter} will tell cypress to hit the enter key
  // this will save our text and close the edit input
  cy.get("input").type("{enter}");

  // Check to make sure our note has been edited correctly
  cy.get("#app > div div").contains("Hello World");
});

The last feature to test is the delete action, a simple right-click on a note button deletes it, the test is very similar to the edit note one, just shorter.

it("right-click on note deletes a note", () => {
  // defensive check to make sure our note is still there
  cy.get("#app > div").children("button").should("have.length", 1);

  // right-click on the note button
  cy.get("#app > div button").rightclick();

  // Check to make sure the note disappeared
  cy.get("#app > div").children("button").should("have.length", 0);
});

And with this all our app functionality has been tested.

Bonus test - login form

Most applications start with a login form, and I didn’t write any code for this extremely common use case.
Below a quick example test including a timeout to load the next page once the authentication is successful.

describe("Login Page", () => {
  it("logs in", () => {
    cy.visit("/login");

    cy.get("input[name='login']").type("test user");
    cy.get("input[name='password']").type("password");
    cy.get("button[type='submit']").click();

    // change /success for the route it should redirect to
    cy.location("pathname", { timeout: 10000 }).should("include", "/success");
  });
});

Closing thoughts

End to end tests can be easier to write than unit test as they don’t care for technologies or code-spaghetiness.

They are also very effective as they are the closest automated tests to the end-user.

The full repository can be found here.