Написание сквозного теста с использованием кипариса
Что такое кипарис?
Cypress — это простой в использовании фреймворк для тестирования. Он имеет множество встроенных функций, облегчающих вашу жизнь, и прост в изучении и использовании.
Cypress очень мощный, вы можете создавать модульные тесты без каких-либо конфигураций или строительных лесов. Вы просто пишете код, а затем параллельно запускаете тесты с нулевыми зависимостями! Вы можете использовать cypress с другими фреймворками, такими как Angular, Vue или React Native!
Это более дружественный к разработчикам инструмент, который использует уникальную технику манипулирования DOM и работает непосредственно в браузере.
Что такое тест e-2-e?
E-2-E тестирование — это тип тестирования, который проверяет все приложение. Его также можно назвать интеграционным тестом, поскольку он проверяет взаимодействие между различными компонентами и модулями.
Это не модульный тест (поскольку он не имеет зависимости от отдельных модулей или компонентов), а скорее сквозной.
Почему стоит выбрать Cypress?
Cypress — это отличный инструмент для использования, когда вы хотите писать сквозные тесты. Он прост в использовании и быстр, поэтому вам не нужно беспокоиться о том, что он медленный или сложный. Cypress также имеет множество плагинов, которые позволяют проводить более сложное тестирование вашего приложения.
Установка cypress с react
Приложение React
Чтобы настроить cypress, сначала создайте приложение react, в этом примере мы будем использовать простое приложение todo с помощью create-react-app.
Сначала создайте react-приложение в папке проекта
npx create-react-app todo
Очистите все ненужные файлы в папке src и коды и создайте структуру папок следующим образом или вы можете использовать свою собственную структуру
в app.jsx добавьте
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;
В 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;
в файле todo.tsx добавить
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;
в app.css добавьте следующие коды для пользовательского интерфейса
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;
}
Запустите npm start
и вот как это должно выглядеть
Из этого простого приложения пользователь должен иметь возможность;
- Создавать новое дело и просматривать его в списке
- Пометить тодо как завершенное или незавершенное
- удалить одно дело
- Фильтровать все, завершенные и незавершенные тодо.
- Удалить все завершенные тодо
Вот что мы будем тестировать с помощью cypress
Установка кипариса
- установите cypress в ваш проект
npm install cypress -D
- Запустите cypress open для настройки cypress в вашем проекте
npx cypress open
- Откроется окно с двумя опциями, e2e тестирование и тестирование компонентов, выберите e2e тестирование.
- Откроется страница конфигурации файлов, прокрутите ее до самого низа и продолжите, cypress создаст все те цены, которые находятся в папке вашего проекта.
- Откроется страница выбора браузера, выберите предпочтительный браузер.
- После конфигурирования файлов в вашем проекте будет создана следующая структура папок
- Создайте папку e2e внутри папки cypress и создайте файл todo.cy.js внутри папки e2e и начните писать тесты.
- Как только вы создадите этот файл, в окне cypress вы должны увидеть созданный вами файл в списке тестов e2e следующим образом
- Чтобы запустить определенный файл теста, нажмите на него, и он запустится.
Напишите свои первые тесты e2e
Перед написанием тестов по любому сценарию сначала нужно изучить команды cypress, есть основные и пользовательские команды. Основные команды — это встроенные команды cypress, а пользовательские команды — это команды, которые вы можете создать. В этом посте мы изучим базовые команды, поскольку пользовательские нам не понадобятся.
Ниже приведен список некоторых базовых команд
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")
Для получения дополнительной информации о базовых командах посетите эту страницу.
Написание тестов для cypress работает почти так же, как и написание любых других тестов.
Прежде всего, запустите приложение, выполнив команду
npm start
Предполагая, что приложение работает на http://localhost:3000
, перед тестированием любого сценария сначала запустите url с помощью команды visit.
describe("create todo spec", () => {
before(() => {
// this will launch the page in cypress browser
cy.visit("http://localhost:3000");
});
})
Напишем тесты в todo.cy.js по сценариям, перечисленным ранее.
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");
});
})
Выполнение команды cypress run должно показать видео всех действий, записанных в тесте.
Эти тесты можно запустить с помощью электронного браузера, это означает, что вы можете запустить эти тесты на терминале ie
npx cypress run
Cypress может выполнять несколько типов тестов, используя основные команды и различные плагины, такие как загрузчик файлов. В компании clickpesa мы используем cypress для проведения e2e тестов.
Ниже перечислены ограничения Cypress
- Нельзя использовать Cypress для одновременной работы двух браузеров.
- Он не обеспечивает поддержку нескольких вкладок.
- Cypress поддерживает только JavaScript для создания тестовых примеров.
- В настоящее время Cypress не поддерживает такие браузеры, как Safari и IE.
- Ограниченная поддержка iFrames.
Наиболее популярной альтернативой Cypress является Selenium
Если вам понравилась эта статья, в наших блогах есть еще много подобных статей, следите за нами на dev.to/clickpesa, medium.com/clickpesa-engineering-blog и clickpesa.hashnode.dev
Счастливого хакинга!!!