Эта заметка является частью серии статей о тестировании E2E. Если вы еще этого не сделали, я рекомендую вернуться и прочитать предыдущие посты, прежде чем продолжить.
- Обзор процесса
- Создание файла тестов и добавление ожидающих тестов
- Примечания к коду
- Использование тестов E2E в качестве «живого» списка дел
- Добавление нашего первого теста
- Прохождение первого теста
- Повышение устойчивости тестов с помощью идентификаторов тестов
- Добавление основного теста пользовательского потока
- Примечания к коду
- Прохождение основного теста пользовательского потока
- Заставляем пройти остальные тесты
- Рефакторинг и уверенная очистка кода
- Далее: Обеспечение автоматического запуска тестов при каждом обновлении кода
Обзор процесса
Мы рассмотрим примерный процесс написания E2E-тестов и затронем некоторые лучшие практики по ходу дела. Вот обзор общего процесса, которому я обычно следую:
- Создайте файл тестов и добавьте ожидающие тесты для всех требований к функциям.
- Напишите минимальный код, необходимый для прохождения каждого теста.
- С помощью программы запуска тестов в режиме наблюдения вернитесь назад, рефакторингом и очисткой кода по мере необходимости.
Если вы знакомы с тестово-ориентированной разработкой (TDD), то все вышесказанное может показаться вам знакомым. Я обнаружил, что написание E2E-тестов — это отличная возможность применить TDD на практике.
В качестве примера мы напишем тесты для формы подписки на рассылку, о которой я рассказывал во вступлении.
Я выбрал его потому, что это небольшая, но все же полноценная функция, а значит, отличная основа для передачи процесса написания тестов E2E.
Для начала мы создадим наш основной тестовый файл.
Создание файла тестов и добавление ожидающих тестов
Если возможно, я предлагаю собрать все тесты для каждой функции в одном тестовом файле, а затем сопоставить название файла с названием теста. Давайте сделаем это сейчас.
(Я предполагаю, что вы уже настроили свое локальное окружение).
Создайте новый файл по адресу cypress/e2e/newsletterSignup.cy.ts
.
Обратите внимание, что в 10-й версии Cypress файлы тестов E2E размещаются в каталоге cypress/e2e
и используют расширение .cy
в имени файла, которое заменяет предыдущее расширение .spec
для тестов E2E.
Добавьте в этот файл следующий код:
// cypress/e2e/newsletterSignup.cy.ts
describe("Newsletter signup", () => { // ** 1 **
beforeEach(() => { // ** 2 **
cy.visit("/newsletter");
});
it("displays a message explaining the form's purpose"); // ** 3 **
it("allows for completing a newsletter signup");
it("allows for submitting the form by using the enter key");
it("displays an error message if input value is not a valid email address");
it("displays an error message if the input field is empty");
it("clears the email input if signup is successful");
it("persists the email input value if there is an error");
});
Примечания к коду
- Это блок
describe
, который является методом, используемым для группировки спецификаций тестов. Я рекомендую согласовывать название этого блока (например, здесь название «Подписка на рассылку») с названием тестового файла и, если возможно, с тем, как эта функция называется в другом месте, например, в истории пользователя. Я предпочитаю следовать этому соглашению, чтобы поощрять наличие только одного набора тестов функций в одном файле, а также чтобы меньше путаться при попытке найти тест. Ценность этого станет более очевидной, когда у вас появится большое количество тестов. - Блок
beforeEach
будет выполняться перед каждым тестом в блоке describe. Это отличное место, чтобы указать программе запуска тестов, куда она должна перейти. Также, когда это применимо, это может быть хорошим местом, чтобы поместить приложение в желаемое состояние, например, используя команду входа в систему. - Это наш список ожидающих проверки тестов. Описания тестов для этих тестов должны вместе составить требования к этой функции. Если вы работаете над историей пользователя, список тестов должен соответствовать вашим критериям приемки.
Использование тестов E2E в качестве «живого» списка дел
На данном этапе ваша консоль Cypress должна выглядеть примерно так:
Перейдите и нажмите на «newsletterSignup», и теперь вы должны увидеть бегунок тестирования со списком ожидающих тестов.
Теперь бегунок тестирования находится в режиме наблюдения, и по мере обновления ваших тестов и исходного кода наши тесты будут автоматически запускаться заново и обновлять тесты до пройденных или провальных.
Лично я считаю, что иметь такой «живой» список дел — это отличный подход, который помогает мне оставаться уверенным в том, что я полностью согласен и создаю то, что от меня требуется, и иметь четкое представление о своем прогрессе.
Добавление нашего первого теста
Мы начнем с того, что проведем первый ожидающий тест, который также является довольно простым.
В cypress/e2e/newsletterSignup.cy.ts
замените первый ожидающий тест следующим кодом:
it("displays a message explaining the form's purpose", () => {
cy.get('[data-test="newsletter"]').contains("Sign up for our newsletter");
});
Тесты Cypress позволяют объединять методы в цепочки, что означает, что они часто бывают однострочными, как в приведенном выше примере.
Тест делится на две части:
- Часть
cy.get
, в которой мы выбираем элемент, который хотим проверить. - Часть
.contains
, которая является одним из многих типов утверждений. В данном случае мы утверждаем, что найдем определенный текстовый блок в ранее выбранном элементе.
(Рискуя вызвать некоторую путаницу, .get
также является утверждением, что элемент существует, а .contains
может также использоваться для выбора элемента для использования в других утверждениях).
Вы могли заметить использование '[data-test="newsletter"]'
для идентификации нашего DOM-элемента newsletter, что может выглядеть немного странно. Мы поговорим об этом более подробно в ближайшее время, но сначала давайте проверим, как проходит вышеприведенный тест.
Прохождение первого теста
Если мы посмотрим на наш бегунок тестирования, то увидим, что тест, который мы добавили выше, не проходит.
Это ожидаемо, так как мы еще не добавили код, чтобы он прошел. Давайте сделаем это сейчас.
Добавьте новый файл по адресу src/pages/newsletter.tsx
. Поскольку мы размещаем файл в специальном каталоге pages
, NextJS создаст новый соответствующий маршрут /newsletter
.
Добавьте следующий код в новый файл:
// src/pages/newsletter.tsx
import type { NextPage } from "next";
const Newsletter: NextPage = () => {
return (
<form data-test="newsletter">
<h3>Sign up for our newsletter!</h3>
</form>
);
};
export default Newsletter;
Если теперь вы вернетесь в бегунок тестирования, то увидите, что рядом с первым тестом стоит зеленая галочка, что означает его прохождение! :celebrate:
Повышение устойчивости тестов с помощью идентификаторов тестов
Обратите внимание на атрибут data-test="newsletter"
, добавленный к тегу form
выше.
<form data-test="newsletter">
"newsletter"
— это наш «идентификатор теста» и то, что мы используем для выбора этого элемента в нашем тесте с помощью селектора [data-test="newsletter"]
.
cy.get('[data-test="newsletter"]')...
Это довольно распространенная схема в E2E-тестировании, где мы используем пользовательский атрибут данных для идентификации элемента (элементов), который мы хотим протестировать. Обратите внимание, что после части data-
вы можете поставить все, что захотите, но я предпочитаю использовать test
.
Зачем нам это нужно?
Я думаю, что самый простой способ понять преимущества использования тестовых идентификаторов — это рассмотреть альтернативные варианты идентификации элементов на странице:
- Использование селектора DOM, например, добавление ID к элементу, например,
<form id="signupForm">
. - Использование имени класса, например
<form class="signup-form">
. - Создание селектора на основе текущей структуры DOM, например
main form h3
.
Основная проблема этих подходов заключается в том, что все они хрупкие.
Каждый раз, когда разработчик прикасается к этому коду, существует вероятность, что он может изменить любой из них из-за необходимости реализации и неосознанно вызвать ложноотрицательный сбой теста.
Использование идентификатора теста позволяет нам отделить код, специфичный для теста, от кода реализации, что делает тесты более устойчивыми к несвязанным изменениям кода.
Добавление основного теста пользовательского потока
Далее добавим тест для основного потока пользователей: заполнение и отправка формы регистрации.
Этот тест будет немного более сложным. Обновите следующий ожидающий тест следующим образом:
// cypress/e2e/newsletterSignup.cy.ts
it("allows for completing a newsletter signup", () => {
const testEmail = "test@email.com";
cy.intercept("POST", "/api/newsletter", (req) => { // ** 1 **
expect(req.body.email).to.contain(testEmail); // ** 2 **
req.reply({ // ** 3 **
statusCode: 200,
body: {
success: true,
message: "Success message",
},
});
});
cy.get('[data-test="emailInput"]').type(testEmail); // ** 4 **
cy.get('[data-test="formSubmit"]').click(); // ** 5 **
cy.get('[data-test="successMessage"]').should("exist"); // ** 6 **
});
Цель этого теста — убедиться, что поток подписки на рассылку «счастливый путь» работает так, как ожидается.
Примечания к коду
В этом тесте происходит довольно много событий. Давайте пройдемся по нему шаг за шагом:
- Здесь мы проверяем, что клиентская сторона отправила на back-end ожидаемую полезную нагрузку.
- Если предыдущее утверждение проходит, мы отправляем ожидаемый успешный ответ.
Прохождение основного теста пользовательского потока
Давайте добавим код, необходимый для прохождения вышеприведенного теста. Обновите src/pages/newsletter.tsx
следующим образом:
import type { NextPage } from "next";
import { useState } from "react";
import { ServerResponse } from "../pages/api/newsletter";
const Newsletter: NextPage = () => {
const [emailInput, setEmailInput] = useState("");
const [confirmMessage, setConfirmMessage] = useState("");
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
setConfirmMessage("");
e.preventDefault();
const raw = await fetch("/api/newsletter", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: emailInput,
}),
});
const response: ServerResponse = await raw.json();
if (response.success) {
setConfirmMessage(response.message);
setEmailInput("");
return;
}
};
return (
<form data-test="newsletter" onSubmit={handleSubmit}>
{confirmMessage && (
<div data-test="successMessage">{confirmMessage}</div>
)}
<h3>Sign up for our newsletter!</h3>
<input
type={"email"}
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
data-test="emailInput"
id="emailInput"
formNoValidate
/>
<input type={"submit"} data-test="formSubmit" />
</form>
);
};
export default Newsletter;
Нам также нужно добавить конечную точку API для отправки данных нашей формы. Создайте новый файл по адресу src/pages/api/newsletter.ts
и добавьте следующее:
import type { NextApiRequest, NextApiResponse } from "next";
export type ServerResponse = {
message: string;
success: boolean;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ServerResponse>
) {
const { email } = req.body;
res.status(200).json({
message: `"${email}" was added to the mailing list. Thanks for signing up!`,
success: true,
});
}
(Я не буду подробно разбирать этот код, так как это не пост о TS или React).
Обратите внимание, что эта конечная точка API на самом деле не хранит отправленный email в хранилище данных. Однако если бы это была настоящая подписка на рассылку, то место для этого, скорее всего, было бы в вышеуказанном файле.
Если мы теперь проверим наш бегунок тестирования, то увидим, что основной тест пользовательского потока («позволяет завершить подписку на рассылку») пройден.
Заставляем пройти остальные тесты
Мы написали минимальный код, необходимый для прохождения текущих тестов. Однако все еще есть несколько требований, которые необходимо выполнить, например, убедиться, что электронная почта сформирована правильно. Завершенные тесты и соответствующий код для их прохождения вы найдете в репозитории github.
Рефакторинг и уверенная очистка кода
Теперь, когда у нас есть проходящие тесты для всех требований к функциям, мы можем воспользоваться плодами наших усилий при рефакторинге.
Например, мы можем захотеть очистить наш код, что, по крайней мере, по моему опыту, часто приводит к непреднамеренным поломкам. Кроме того, я не добавил ничего нового в плане стилизации, и это тоже то, что мы теперь можем делать с гораздо меньшим опасением что-то сломать.
Далее: Обеспечение автоматического запуска тестов при каждом обновлении кода
Теперь, когда мы написали несколько тестов, давайте убедимся и запустим их в работу. В следующем посте мы покажем, как убедиться, что ваш полный набор тестов запускается каждый раз, когда вы добавляете новый код, и почему это важно.