Write end to end test using react cypress
What is cypress?
Cypress is a simple to use testing framework. It has many built-in features to make your life easier and it's easy to learn and use.
Cypress is very powerful, you can create unit tests without any configuration or scaffolding. You just write your code and then run tests in parallel with zero dependencies!. You can use cypress with other frameworks like Angular, Vue or React Native too!
It is a more developer-friendly tool that uses a unique DOM manipulation technique and operates directly in the browser.
What is the e-2-e test?
E-2-E testing is a type of test that tests the entire application. It can be also known as an integration test, because it tests the interaction between different components and modules.
It is not a unit test (as it does not have any dependency on individual modules or components), but rather an end to end one.
Why choose cypress?
Cypress is a great tool to use when you want to write end to end tests. It’s easy to use and fast, so you don’t need to worry about it being slow or complicated. Cypress also has many plugins that allow you to do more advanced testing on your app.
Setup cypress with react
React app
To set up cypress first create your react app, in this example we will be using a simple todo app using a create-react-app.
First create react app in your project folder
npx create-react-app todo
Clean all the unwanted files in the src folder and codes and create a folder structure like this or you can use your own structure
in app.jsx add
import { useEffect, useState } from "react";
import Addtodo from "./addtodo";
import "./app.css";
import Todo from "./todo";
const App = () => {
const [todos, setTodos] = useState([]);
const [keyword, setKeyword] = useState("All");
const [filteredTodos, setFilteredTodos] = useState(todos);
const addTodo = (todo) => {
setTodos([todo, ...todos]);
setKeyword("All");
};
const updateTodo = (id) => {
let todosCopy = todos;
const todoIndex = todosCopy.findIndex((el) => el.id === id);
const todo = todosCopy.find((el) => el.id === id);
todosCopy[todoIndex] = {
...todo,
isActive: !todo.isActive,
};
setTodos([...todosCopy]);
};
const deleteTodo = (id) => {
const newTodos = todos.filter((el) => el.id !== id);
setTodos(newTodos);
};
useEffect(() => {
filterTodos(keyword);
}, [todos, keyword]);
const filterTodos = (key) => {
if (key === "All") {
setFilteredTodos(todos);
setKeyword("All");
return;
}
if (key === "Active") {
setFilteredTodos(todos.filter((el) => el.isActive === true));
setKeyword("Active");
return;
}
if (key === "Completed") {
setFilteredTodos(todos.filter((el) => el.isActive === false));
setKeyword("Completed");
return;
}
if (key === "Clear") {
setTodos(todos.filter((el) => el.isActive === true));
setKeyword("All");
}
};
return (
<div className="App">
<div className="container">
<Addtodo addTodo={addTodo} />
<ul>
{filteredTodos.map((todo) => (
<Todo
todo={todo}
key={todo.id}
updateTodo={updateTodo}
deleteTodo={deleteTodo}
/>
))}
</ul>
<div className="actions">
<span>
{todos.filter((el) => el.isActive === true).length} Active items
left
</span>
<div>
<button
className="filter-all"
onClick={() => {
filterTodos("All");
}}
>
All
</button>
<button
className="filter-active"
onClick={() => {
filterTodos("Active");
}}
>
Active
</button>
<button
className="filter-completed"
onClick={() => {
filterTodos("Completed");
}}
>
Completed
</button>
<button
className="delete-all"
onClick={() => {
filterTodos("Clear");
}}
>
Clear Completed
</button>
</div>
</div>
</div>
</div>
);
};
export default App;
In addTodo.jsx
import { useState } from "react";
const Addtodo = ({ addTodo }) => {
const [todo, setTodo] = useState({
id: "",
title: "",
isActive: true,
});
const { title } = todo;
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
todo?.title?.trim()?.length > 0 && addTodo(todo);
setTodo({
id: "",
title: "",
isActive: true,
});
}}
>
<input
placeholder="New todo"
value={title}
onChange={(e) => {
let id = new Date();
id = id.getTime();
if (e.target.value?.trim().length > 0)
setTodo({
...todo,
id,
title: e.target.value,
});
}}
/>
<button className="submit-button" title="Add Todo">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.75 5.25V0H5.25V5.25H0V6.75H5.25V12H6.75V6.75H12V5.25H6.75Z"
fill="white"
/>
</svg>
</button>
</form>
</div>
);
};
export default Addtodo;
in todo.tsx add
const Todo = ({ todo, deleteTodo, updateTodo }) => {
const { title, id, isActive } = todo;
return (
<li className={isActive ? "incomplete" : "complete"}>
<div>
<button
className={isActive ? "mark mark-complete" : "mark mark-incomplete"}
onClick={() => {
updateTodo(id);
}}
title={isActive ? "Mark complete" : "Mark incomplete"}
>
<svg
width="13"
height="10"
viewBox="0 0 13 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.455 0.45498C10.6663 0.253813 10.9475 0.1426 11.2392 0.144808C11.531 0.147015 11.8104 0.262471 12.0187 0.466812C12.2269 0.671152 12.3476 0.948396 12.3553 1.24004C12.363 1.53169 12.2571 1.81492 12.06 2.02998L6.07499 9.51498C5.97208 9.62583 5.84787 9.71478 5.70979 9.77653C5.57171 9.83828 5.4226 9.87155 5.27137 9.87435C5.12014 9.87715 4.9699 9.84942 4.82963 9.79283C4.68936 9.73624 4.56194 9.65194 4.45499 9.54498L0.485992 5.57598C0.375462 5.47299 0.286809 5.34879 0.225321 5.21079C0.163833 5.07279 0.13077 4.92382 0.128105 4.77276C0.12544 4.62171 0.153227 4.47167 0.209808 4.33158C0.26639 4.1915 0.350607 4.06425 0.457435 3.95742C0.564263 3.85059 0.691514 3.76638 0.831596 3.7098C0.971678 3.65321 1.12172 3.62543 1.27278 3.62809C1.42383 3.63076 1.5728 3.66382 1.7108 3.72531C1.8488 3.7868 1.973 3.87545 2.07599 3.98598L5.21699 7.12548L10.4265 0.48798C10.4359 0.476432 10.4459 0.465414 10.4565 0.45498H10.455Z"
fill="green"
/>
</svg>
</button>
{title}
</div>
<button
onClick={() => {
deleteTodo(id);
}}
className="delete"
title="Delete todo"
>
<svg
width="16"
height="16"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 4H12C12 3.46957 11.7893 2.96086 11.4142 2.58579C11.0391 2.21071 10.5304 2 10 2C9.46957 2 8.96086 2.21071 8.58579 2.58579C8.21071 2.96086 8 3.46957 8 4V4ZM6 4C6 2.93913 6.42143 1.92172 7.17157 1.17157C7.92172 0.421427 8.93913 0 10 0C11.0609 0 12.0783 0.421427 12.8284 1.17157C13.5786 1.92172 14 2.93913 14 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5C20 5.26522 19.8946 5.51957 19.7071 5.70711C19.5196 5.89464 19.2652 6 19 6H18.118L17.232 16.34C17.1468 17.3385 16.69 18.2686 15.9519 18.9463C15.2137 19.6241 14.2481 20.0001 13.246 20H6.754C5.75191 20.0001 4.78628 19.6241 4.04815 18.9463C3.31002 18.2686 2.85318 17.3385 2.768 16.34L1.882 6H1C0.734784 6 0.48043 5.89464 0.292893 5.70711C0.105357 5.51957 0 5.26522 0 5C0 4.73478 0.105357 4.48043 0.292893 4.29289C0.48043 4.10536 0.734784 4 1 4H6ZM13 10C13 9.73478 12.8946 9.48043 12.7071 9.29289C12.5196 9.10536 12.2652 9 12 9C11.7348 9 11.4804 9.10536 11.2929 9.29289C11.1054 9.48043 11 9.73478 11 10V14C11 14.2652 11.1054 14.5196 11.2929 14.7071C11.4804 14.8946 11.7348 15 12 15C12.2652 15 12.5196 14.8946 12.7071 14.7071C12.8946 14.5196 13 14.2652 13 14V10ZM8 9C8.26522 9 8.51957 9.10536 8.70711 9.29289C8.89464 9.48043 9 9.73478 9 10V14C9 14.2652 8.89464 14.5196 8.70711 14.7071C8.51957 14.8946 8.26522 15 8 15C7.73478 15 7.48043 14.8946 7.29289 14.7071C7.10536 14.5196 7 14.2652 7 14V10C7 9.73478 7.10536 9.48043 7.29289 9.29289C7.48043 9.10536 7.73478 9 8 9V9ZM4.76 16.17C4.8026 16.6694 5.03117 17.1346 5.40044 17.4735C5.76972 17.8124 6.25278 18.0003 6.754 18H13.246C13.7469 17.9998 14.2294 17.8117 14.5983 17.4728C14.9671 17.134 15.1954 16.6691 15.238 16.17L16.11 6H3.89L4.762 16.17H4.76Z"
fill="red"
/>
</svg>
</button>
</li>
);
};
export default Todo;
in app.css add the following codes for UI
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 90vh;
background-color: #1e272e;
color: white;
}
.container {
background-color: #2b343b;
padding: 2rem;
max-width: 450px;
width: 90%;
min-width: 250px;
margin: auto;
}
form {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1.5rem;
}
input {
height: 40px;
width: calc(100% - 80px);
background-color: #1e272e;
border: none;
border-bottom: 1px solid #1e272e;
padding: 0 1rem;
transition: 0.25s ease-out;
color: white;
}
button {
background: none;
color: white;
border: none;
}
button.submit-button {
width: 80px;
height: 40px;
background-color: cadetblue;
color: white;
border: none;
}
input:focus {
border: none;
background-color: #2b343b;
border-bottom: 1px solid grey;
}
input:focus,
button:focus {
outline: none !important;
}
ul {
list-style: none;
color: white;
padding-inline-start: 0;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
ul::-webkit-scrollbar {
width: 0.3rem;
}
ul::-webkit-scrollbar-track {
background: gray;
}
ul::-webkit-scrollbar-thumb {
background: darkgrey;
border-radius: 0.5em;
}
ul li {
display: flex;
justify-content: space-between;
background-color: #1e272e;
padding: 0.5rem;
margin: 0.5rem 0;
transition: 0.22s;
}
ul li.complete {
text-decoration: line-through;
opacity: 0.5;
}
.actions {
display: flex;
justify-content: space-between;
margin-top: 2rem;
}
.actions button {
color: lightgray;
}
.actions button.delete-all {
color: red;
}
Run npm start
and this is how it should look
From this simple app, user should be able to;
- Create a new todo and preview the new todo on the list
- Mark a todo as complete or incomplete
- Delete a single todo
- Filter all, complete and incomplete todos
- Delete all complete todos
These are what we'll be testing using cypress
Cypress setup
- install cypress in your project
npm install cypress -D
- Run cypress open to configure cypress in your project
npx cypress open
- This will open a window with two options, e2e testing and component testing, select e2e testing
- This will open configure files page, scroll to the bottom and continue, cypress will generate all those price in your project folder
- This will open browser selection page, select your preferable browser
- The following folder structure will be created in your project after configuring files
- Create e2e folder inside cypress folder and create todo.cy.js inside e2e folder and start writing the tests.
- Once you create this file, in cypress window you should see a file you created in e2e test list like this
- to run a specific test file, click on the file and it should run.
Write your first e2e tests
Before writing test on any scenario first we need to learn cypress commands, there are basic custom commands. basic commands are the built in cypress commands while custom commands are the commands you can create. For this post we'll learn about basic commands since we won't need any custom command.
The following is the list of some basic commands
cy.visit("url") // this launches url
cy.wait(300) // Wait for a certain time in milliseconds or for an aliased element prior to moving the following step.
cy.get("selector") // this returns single or multiple elements with provided selector i.e classname, tagname, id or title
// class selector can be written as cy.get(".classname")
cy.contains("text") // returns a single or multiple elements that contains text
cy.click() // clicks the selected element
cy.dblclick() // double clicks the selected element
cy.document() // It obtains window.document on the active page.
cy.type("text") // type text on a selected input
cy.get(".classname").eq(0) // for multiple elements you can select a specific element depending on the position of such element using eq command with element index
cy.should("exist") // checks if a selected element exists or not, there are multiple parameters thet can be used inside should for varios checks
// i.e not.exist, be.visible, be.enabled
// it can be used with an alias .and for multiple checks ie
// cy.get(".classname").should("exist).and("be.visible")
For more basic commands visit here.
Writing cypress tests works almost the same as writing any other tests.
Before all start the app by running
npm start
Assuming the app runs on http://localhost:3000
, before any testing any scenario first launch a url using visit command.
describe("create todo spec", () => {
before(() => {
// this will launch the page in cypress browser
cy.visit("http://localhost:3000");
});
})
Lets write tests in todo.cy.js by scenarios listed before.
describe("create todo spec", () => {
before(() => {
// this will launch the page in cypress browser
cy.visit("http://localhost:3000");
});
// 1. Create a todo scenario
it("successfully create a todo", () => {
// find an input by tagname
cy.get("input").type("Sprint meeting");
// submit form by title of submit button
cy.get("[title='Add Todo']").click();
// create another todo
cy.get("input").type("Coding");
// submit form by classname of submit button
cy.get(".submit-button").click();
// check if new todos exist
cy.contains("Sprint meeting").should("exist");
cy.contains("Coding").should("exist");
});
// 2. Mark todo as complete / incomplete
it("Should mark todo", () => {
// mark a first todo as complete
cy.get("button.mark-complete").eq(0).click();
// select using css selector
cy.get("li:nth-child(2) button.mark").click();
});
// 3. Delete a todo
it("Should delete a todo", () => {
cy.get("li:nth-child(2) button.delete").click();
})
// 4. Filter all, complete and incomplete todos
it("should filter between all, complete and incomplete todos", () => {
// create multiple todos
cy.get("input").type("Sprint meeting");
cy.get("[title='Add Todo']").click();
cy.get("input").type("Code");
cy.get("[title='Add Todo']").click();
cy.get("input").type("Exercising");
cy.get("[title='Add Todo']").click();
cy.get("input").type("Read");
cy.get("[title='Add Todo']").click();
cy.get("input").type("Eat");
cy.get("[title='Add Todo']").click();
cy.get("input").type("Code");
cy.get("[title='Add Todo']").click();
// mark some as complete
cy.get("li:nth-child(2) button.mark").click();
cy.get("li:nth-child(4) button.mark").click();
cy.get("li:nth-child(6) button.mark").click();
// filter complete only
cy.get("button.filter-completed").click();
// check if only complete exists
cy.get(".complete").should("exist");
cy.get(".incomplete").should("not.exist");
// filter incomplete only
cy.get("button.filter-active").click();
cy.get(".complete").should("not.exist");
cy.get(".incomplete").should("exist");
// filter all
cy.get("button.filter-all").click();
cy.get(".complete").should("exist");
cy.get(".incomplete").should("exist");
});
// 5. Delete all complete todos
it("should delete all completed todos", () => {
// find and click delete all completed todos
cy.get("button.delete-all").click();
cy.get(".complete").should("not.exist");
});
})
Running cypress run command should show a video of all actions written on the test.
These tests can run using electron browser, this means that you can run these tests on terminal ie
npx cypress run
Cypress can do multiple types tests using basic commands and different plugins such as file uploader. At clickpesa we use cypress to do the e2e tests making sure we have
The following are limitations of cypress
- One cannot use Cypress to drive two browsers at the same time.
- It doesn't provide support for multi-tabs.
- Cypress only supports JavaScript for creating test cases.
- Cypress doesn't provide support for browsers like Safari and IE at the moment.
- Limited support for iFrames.
The most popular alternative to cypress is Selenium
If you like this article there are more like this in our blogs, follow us on dev.to/clickpesa, medium.com/clickpesa-engineering-blog and clickpesa.hashnode.dev
Happy Hacking!!
Top comments (2)
Playwright is better with minimal dependencies. There are no limitation as mentioned..
I will check it out, probably will write an article about it too