End to End testing can be slow and boring. Cypress has changed the way we test. Learn how to test your applications in this tutorial.
Cypress Tutorial for Beginners: requirements
To follow along you need a working installation of Node.js on your system. Also, a basic understanding of a newer version of JavaScript is a plus.
What is Cypress? What is End to End Testing?
End to End Testing, or UI testing is one the many approaches for testing a web application.
An end to end test is supposed to check whether a web application works as expected or not, by testing the so called user flow.
Is End to End Testing important? Yes it is. But nobody likes E2E tests. They can be slow, cumbersome and expensive to write.
On the other hand testing gives you confidence. Would you ship a broken product to your users on purpose?
Enter Cypress: a Javascript End to End testing framework. It will make your life easier.
Disclaimer
Before purists yell at me: I know the subtle line between End to End Testing, UI testing, integration testing, and so on.
And for you dear reader: testing terminology is so blurry that I can't even .... If it's your first time with JavaScript testing I recommend reading Jest Tutorial for Beginners: Getting Started With Jest for JavaScript Testing for an introduction on unit testing and terminology.
When you're done come back here for End to End Testing.
Cypress tutorial for beginners: setting up the project
To start off create a new folder, I called mine cypress-tutorial, move into it an initialize a new JavaScript project:
mkdir cypress-tutorial && cd $_
npm init -y
Inside this folder create two new files. An HTML document in index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cypress tutorial for beginners</title>
</head>
<body>
<main>
<form>
<div>
<label for="name">Name</label>
<input type="name" required name="name" id="name" />
</div>
<div>
<label for="email">Email</label>
<input type="email" required name="email" id="email" />
</div>
<div>
<label for="message">Your message</label>
<textarea id="message" name="message" required></textarea>
</div>
<div>
<button type="submit">SEND</button>
</div>
</form>
</main>
</body>
<script src="form.js"></script>
</html>
It is an HTML form with a bunch on inputs and a textarea. Next up create a JavaScript file in form.js with a minimal logic for handling form submission:
const form = document.forms[0];
form.addEventListener("submit", event => {
event.preventDefault();
});
Note that I won't add styles to keep things simple. And with this simple project in place we're ready to install Cypress.
Installing Cypress
To install Cypress, still in the project folder, run:
npm i cypress --save-dev
Give it a minute (it needs to download the binary) and then run:
node_modules/.bin/cypress open
Cypress will start for the first time and a bunch of new folder will appear in your project. You can remove the example folder, but look a moment into it because there are some nice examples.
Close the window for now and head over the next section.
Starting the project
To serve the project on the local machine make sure to have a newer version of Node.js installed and then run:
npx serve
This will spin a development server at http://localhost:5000/. Head over the link and you should see our form:
serve is a nice NPM package for development. And now time to write our first test!
Cypress tutorial for beginners: writing your first test
Create a new file in cypress/integration/form.spec.js and write your first block:
describe("Form test", () => {
//
});
describe is a Cypress method (borrowed from Mocha) for containing one or more related tests. Every time you start writing a new suite of tests for a functionality wrap it in a describe block.
As you can see it takes two arguments: a string for describing the test suite and a callback function for wrapping the actual test.
Next up we're going to meet another function called it which is the actual test block:
describe("Form test", () => {
it("Can fill the form", () => {
//
});
});
If you know already Jest you may recall that it takes it or test interchangeably. That's not the case with Cypress. it is the only accepted block.
And now time for a smoke test! In the it block write:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
});
});
Here cy is Cypress itself and visit is a Cypress method for browsing to a given path.
get instead is a method for selecting elements in the page. With this code we tell Cypress "go grab the form in the page".
In a minute we'll see Cypress in action, but first, a bit of configuration!
Configuring Cypress
To streamline things a bit we're going to configure Cypress. To begin with open up package.json and create a script named e2e pointing to the Cypress binary:
"scripts": {
"e2e": "cypress open"
},
Next up open cypress.json and configure the base url:
{
"baseUrl": "http://localhost:5000"
}
With this option we tell Cypress to visit our development url. (5000 is the default port for the serve package).
And now we're ready to launch your first test!
Cypress tutorial for beginners: running the test
Ready? With the development server still running in a terminal:
npx serve
open up another terminal and run:
npm run e2e
You should see Cypress opening a browser and going through the page:
That's your first test passing! Both visit and get are Cypress commands which act also as implicit assertions, that is, if the element is in the page Cypress will consider the test passed.
Now let's continue extending your test to see if the user can fill the form:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]').type("Molly");
});
});
Here's another Cypress command: type, which unsurprisingly types into our first text input. Also notice the CSS selector for getting the input element.
While there let's also add another command: should. This command creates an assertion and is used for example to check if an input is updating its state as expected:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]')
.type("Molly")
.should("have.value", "Molly");
});
});
Notice have.value. If you're new to this concept you can learn more about assertions here.
And with a minimal test in place let's continue in the next section.
Cypress tutorial for beginners: more tests and a submit
To continue our test we can check the email input:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]')
.type("Molly")
.should("have.value", "Molly");
cy.get('input[name="email"]')
.type("molly@dev.dev")
.should("have.value", "molly@dev.dev");
});
});
Also we can type into the textarea:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]')
.type("Molly")
.should("have.value", "Molly");
cy.get('input[name="email"]')
.type("molly@dev.dev")
.should("have.value", "molly@dev.dev");
cy.get("textarea")
.type("Mind you if I ask some silly question?")
.should("have.value", "Mind you if I ask some silly question?");
});
});
If you left Cypress open the test should watch your changes and run automatically:
How nice! As icing on the cake let's test form submission with submit:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]')
.type("Molly")
.should("have.value", "Molly");
cy.get('input[name="email"]')
.type("molly@dev.dev")
.should("have.value", "molly@dev.dev");
cy.get("textarea")
.type("Mind you if I ask some silly question?")
.should("have.value", "Mind you if I ask some silly question?");
cy.get("form").submit();
});
});
The test should keep passing without any problem. One thing you can notice are these self-describing commands: type, submit. It's plain english.
Now let's go a bit fancy in the next section with XHR request testing.
Stubbing XHR requests with Cypress
Among all the things Cypress is also able to intercept AJAX request. This approach is called stubbing.
Stubbing is convenient when working in development where you can choose to return fake response to your AJAX requests.
To demonstrate this feature let's add a new piece of code inside our test:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
// omitted for brevity
cy.server();
cy.route({
url: "/users/**",
method: "POST",
response: { status: "Saved", code: 201 }
});
cy.get("form").submit();
});
});
Here cy.server spins up a "virtual" server while cy.route is used for configuring a fake API endpoint.
Now let's add another test to check things out: after the user submits the form we want to test that the fake API is responding. Why so?
Stubbing is useful because we can bypass the real API altogether in development. Let's extend the test with cy.contains:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
// omitted for brevity
cy.server();
cy.route({
url: "/users/**",
method: "POST",
response: { status: "Form saved!", code: 201 }
});
cy.get("form").submit();
cy.contains("Form saved!");
});
});
The test is expected to fail because there is no logic in place for sending the form to an API. In the next section we'll make the test pass.
Sending form data to an API
Bear with me in this section! At the time of writing Cypress couldn't intercept Fetch requests. We're going to use XMLHttpRequest instead. (Check this out for an introduction).
Open up form.js and implement the logic:
const form = document.forms[0];
form.addEventListener("submit", event => {
event.preventDefault();
new FormData(form);
});
document.addEventListener("formdata", event => {
const body = Object.fromEntries(event.formData.entries());
const jsonBody = JSON.stringify(body);
const request = new XMLHttpRequest();
request.open("POST", "https://jsonplaceholder.typicode.com/users/");
request.send(jsonBody);
});
In this snippet I'm using the formdata event. The event is dispatched when we call new FormData.
In the event listener we build an object with fromEntries (ECMAScript 2019). The we send the data to an API.
To make the test pass we also need to get the response back from the API and save it to the document. For doing so we can listen on the onload event of XMLHttpRequest:
// omit
document.addEventListener("formdata", event => {
const body = Object.fromEntries(event.formData.entries());
const jsonBody = JSON.stringify(body);
const request = new XMLHttpRequest();
request.open("POST", "https://jsonplaceholder.typicode.com/users/");
request.send(jsonBody);
// get the response
request.onload = function() {
const jsonResponse = JSON.parse(this.response);
};
});
Finally we can dangerously (just for keeping things simple) save the response in the page (please don't never ever do this in a serious codebase):
// omit
request.onload = function() {
const jsonResponse = JSON.parse(this.response);
document.body.innerHTML += `Response from the server: ${jsonResponse.status}`;
};
And now it's time to see the test passing!
Stubbing XHR requests with Cypress: a passing test
To recap here's the complete test in cypress/integration/form.spec.js:
describe("Form test", () => {
it("Can fill the form", () => {
cy.visit("/");
cy.get("form");
cy.get('input[name="name"]')
.type("Molly")
.should("have.value", "Molly");
cy.get('input[name="email"]')
.type("molly@dev.dev")
.should("have.value", "molly@dev.dev");
cy.get("textarea")
.type("Mind you if I ask some silly question?")
.should("have.value", "Mind you if I ask some silly question?");
cy.server();
cy.route({
url: "/users/**",
method: "POST",
response: { status: "Form saved!", code: 201 }
});
cy.get("form").submit();
cy.contains("Form saved!");
});
});
And here's the complete code for form.js:
const form = document.forms[0];
form.addEventListener("submit", event => {
event.preventDefault();
new FormData(form);
});
document.addEventListener("formdata", event => {
const body = Object.fromEntries(event.formData.entries());
const jsonBody = JSON.stringify(body);
const request = new XMLHttpRequest();
request.open("POST", "https://jsonplaceholder.typicode.com/users/");
request.send(jsonBody);
// get the response
request.onload = function() {
const jsonResponse = JSON.parse(this.response);
document.body.innerHTML += `Response from the server: ${jsonResponse.status}`;
};
});
One thing to keep in mind is that the real API would not likely return the same shape of our fake stub. When developing a real app you do need to adapt your tests to the real system.
But for now we're good and if you kept Cypress opened you should already see the test passing:
You can see a routes section at the top left and XHR stub in the test output, sign that Cypress has intercepted the POST request.
That's one of the best features of Cypress, not counting the dozens of commands and assertions ready to use.
With stubbing we can conclude the tutorial. Great job!
Cypress tutorial for beginners: conclusions
I hope you learned something new with this tutorial and you'll apply these concepts to your next project! Testing is important!
End to end testing shouldn't be hard: Cypress makes it pleasing and enjoyable. The team at Cypress really nailed it.
Plus, the documentation is pure gold: Cypress Docs are filled up with best practices and examples.
Thanks for reading!
Originally published on my blog
Resources
Stefano Magni has a lot of nice testing tips in his UI testing best practices.
Top comments (4)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.