Write your first end-to-end test in 5 minutes
Francesco Agnoletto
Posted on April 29, 2020
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.
originally posted on decodenatura
Posted on April 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.