Асинхронные операции: от теории к практике
Асинхронное программирование — это одна из самых мощных сторон JavaScript. Оно позволяет нам, например, отправлять запросы на сервер, не замораживая интерфейс и не заставляя пользователя скучать, глядя на "зависшее" приложение. Асинхронные операции включают в себя работу с fetch, таймеры setTimeout, setInterval, и анимации. Однако из-за своей природы (внезапное завершение в "непредсказуемый момент") такие операции могут быть сложными для тестирования.
Главные вызовы тестирования асинхронных операций:
- Ожидание завершения операции. Тесты отрабатывают быстрее, чем асинхронные операции, что может привести к ошибкам.
- Обработка состояния загрузки. Мы должны проверить, как приложение реагирует на состояние "loading" (загрузка).
- Обработка ошибок. Сервер может быть недоступен, данные могут прийти в неверном формате, или что-то другое может пойти не так. Мы обязаны убедиться, что всё правильно обрабатывается.
Cypress: наш лучший друг для тестирования асинхронности
Одной из причин популярности Cypress является его встроенная способность "ждать" завершения асинхронных операций. Вам не нужно вручную управлять ожиданиями setTimeout или явно прописывать "паузы". Cypress делает за нас грязную работу благодаря цепочной системе команд.
Вот как Cypress автоматически управляет ожиданием:
- Cypress отслеживает изменения в DOM.
- Он ждёт, пока элементы станут доступными на странице.
- Он ждёт завершения асинхронных действий, таких как API-запросы.
Поэтому, с точки зрения тестирования асинхронных процессов, Cypress заметно упрощает жизнь.
Пишем тесты для асинхронных операций: шаг за шагом
1. Тестируем загрузку данных с API
Возьмём простой пример. Скажем, у нас есть компонент, который загружает данные о пользователях с API и отображает их в списке.
Код компонента:
// components/Users.tsx
import React, { useEffect, useState } from "react";
interface User {
id: number;
name: string;
}
const Users: React.FC = () => {
const [users, setUsers] = useState<User[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Эмулируем API-запрос
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
})
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default Users;
Код теста:
// cypress/e2e/users_spec.cy.ts
describe("Users Component", () => {
it("should display a loading state", () => {
cy.visit("/users"); // Переход на страницу компонента Users
cy.contains("Loading...").should("be.visible"); // Проверяем, что отображается текст "Loading..."
});
it("should display users after API call", () => {
cy.intercept("GET", "https://jsonplaceholder.typicode.com/users", {
fixture: "users.json", // Подставляем фикстуру вместо реального API
}).as("getUsers");
cy.visit("/users");
cy.wait("@getUsers"); // Ждём завершения запросов
cy.get("li").should("have.length", 10); // Убеждаемся, что на странице 10 пользователей
});
it("should handle API errors gracefully", () => {
cy.intercept("GET", "https://jsonplaceholder.typicode.com/users", {
statusCode: 500, // Симулируем серверную ошибку
}).as("getUsersError");
cy.visit("/users");
cy.wait("@getUsersError");
cy.contains("Error").should("be.visible");
});
});
2. Работа с асинхронными кнопками
Теперь усложним задачу. Пусть у нас будет кнопка, которая выполняет асинхронное действие, например, отправляет данные на сервер.
Код компонента:
// components/SubmitButton.tsx
import React, { useState } from "react";
const SubmitButton: React.FC = () => {
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const handleClick = () => {
setStatus("loading");
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
body: JSON.stringify({ title: "foo", body: "bar", userId: 1 }),
headers: { "Content-type": "application/json; charset=UTF-8" },
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to submit");
}
return response.json();
})
.then(() => setStatus("success"))
.catch(() => setStatus("error"));
};
return (
<div>
<button onClick={handleClick} disabled={status === "loading"}>
{status === "loading" ? "Submitting..." : "Submit"}
</button>
{status === "success" && <p>Submission successful!</p>}
{status === "error" && <p>Error occurred during submission!</p>}
</div>
);
};
export default SubmitButton;
Код теста:
// cypress/e2e/submit_button_spec.cy.ts
describe("SubmitButton Component", () => {
it("should display loading state while submitting", () => {
cy.visit("/submit"); // Переход на страницу с компонентом SubmitButton
cy.get("button").click();
cy.contains("Submitting...").should("be.visible"); // Проверяем, что отображается текст "Submitting..."
});
it("should display success message after successful submission", () => {
cy.intercept("POST", "https://jsonplaceholder.typicode.com/posts", {
statusCode: 201, // Симулируем успешный ответ сервера
}).as("postSubmit");
cy.visit("/submit");
cy.get("button").click();
cy.wait("@postSubmit");
cy.contains("Submission successful!").should("be.visible");
});
it("should display error message after failed submission", () => {
cy.intercept("POST", "https://jsonplaceholder.typicode.com/posts", {
statusCode: 500, // Симулируем ошибку на сервере
}).as("postSubmitError");
cy.visit("/submit");
cy.get("button").click();
cy.wait("@postSubmitError");
cy.contains("Error occurred during submission!").should("be.visible");
});
});
Логирование и анализ ошибок
Ошибки неизбежны, особенно в сложных приложениях. Cypress позволяет нам эффективно их логировать. Для этого вы можете добавлять дополнительные cy.log() команды, чтобы пояснять происходящее. Например:
cy.log("Проверяем статус успешного сабмита").contains("Submission successful!");
Асинхронные операции могут быть сложными, но с помощью Cypress их становится гораздо проще тестировать. Главное — держать всё в порядке: использовать интерсепторы intercept, фикстуры и грамотно обрабатывать состояния загрузки и ошибок. Переходите к следующим шагам в тестировании, а пока наслаждайтесь гладко работающими тестами вашего приложения!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ