- E2E Практики
- Что вообще проверяем?
- Что мы делаем
- Даже не пытаемся тестировать
- Подмена бекенда
- Mock Service Worker (msw)
- Исправление ответов сервера
- Page Object Pattern
- Page objects
- Преимущества
- Selenium
- Правила
- Минусы e2e
E2E Практики
Hexlet
Что вообще проверяем?
- Соответствие макету
- поддержка retina-мониторов
- pixel-perfect
- контраст / следование style guides
- Проверка на разных разрешениях экрана
- десктопной
- мобильной
- адаптивных
- HTML / CSS / JS
- Шрифты
- Работа в разных окружениях
- кроссбраузерность
- работа на разных устройствах
- работа на разных операционных системах
- корректная работа с разной скоростью интернета
- корректная работа при включенном расширением AdBlock в браузере
- анимация / прокрутка / sticky элементы / плавность
- Контент
- большой текст
- орфографии
- изображения
Что мы делаем
- определяем пользовательские сценарии
- логин
- создание аккаунта
- отправка сообщений
- покупки
- автоматизируем сценарии
- тестируем тесты
- учитываем кроссбраузерность
Даже не пытаемся тестировать
- CAPTCHA
- Отключаем капчу в тестовом окружении
- Добавьте хук, позволяющий тестам обходить капчу
Двухфакторная аутентификация
- Отключаем 2FA в тестовом окружении
- Отключаем 2FA для определенных пользователей в тестовом окружении Вы можете использовать учетные данные этого пользователя при автоматизации
- Отключите 2FA для входа в систему с определенных IP-адресов Мы можем установить эти IP-адреса для наших тестовых машин --- # Даже не пытаемся тестировать
вход на сервисы вроде Gmail и Facebook, с помощью WebDriver это делать не стоит
- это против условий использования этих сайтов
- вы рискуете потерять учетную запись
- это медленно и ненадежно
- используйте сервис, предоставляющий API для создания тестовых учетных записей
- работа с API может усложнить работу, но это окупится скоростью, надежностью и стабильностью
геттеры / сеттеры
нечто нерелевантное / внешнее / постороннее
- Поддержка X браузеров на Y платформах - затратно
- Ведет к ловушке поддержки X×Y реализаций
- Делайте ваши тесты как можно более устойчивыми === тесты не будут сразу ломаться при любом изменении
- Ориентируйтесь на перспективу конечного пользователя
- Мыслите как пользователь
- Сосредоточьтесь на особенностях приложения, а не на его реализации
- Чего пытается достичь пользователь?
- Легко ли найти то, что он(а) ищет?
- Достигнет ли пользователь своей цели в несколько простых шагов?
- Избегайте нагромождений селекторов
- Убедитесь, что у элемента есть стабильный селектор, который не изменится в следующей версии приложения
- Выбирайте элементы страницы с умом
- ID
- CSS селекторы
- data-аттрибуты
- Доступность (aria)
- Тесты не должны зависеть друг от друга
- Не игнорируйте неустойчивые тесты, которые возвращают разные результаты без каких-либо изменений в коде
- Прогоняйте тесты еще раз, прежде чем заводить issue
- Убедитесь, что у вас есть подходящие тестовые данные
- Напишите отчет
- Проведите дымовое тестирование
- Разработайте наборы тестов на согласованность
- Постройте хорошую организационную структуру
- Нашли баг? Напишите тест, а затем исправьте его
- Ждите, не спите
- Тест должен быть простым
- Используйте CI / уведомления
- Используйте линтеры, следуйте стилям кодирования и т.д.
eslint-plugin-jest
может предупреждать, когда в тесте нет утверждения (assertion)- Вы можете группировать тесты тегам вроде #smoke
- Антипаттерн: Вы читаете отчет => просматриваете код
Подмена бекенда
Mock Service Worker (msw)
- Передовой мок-API
- Перехватывает запросы на сетевом уровне, а не на уровне приложений
- Вы можете использовать axios, fetch, xhr, что угодно
// handlers.js
import { rest } from 'msw';
export const handlers = [
rest.post('/login', (req, res, ctx) => {
// Сохраняем в сессии статус аутентификации пользователя
sessionStorage.setItem('is-authenticated', 'true');
return res(
// Отвечаем кодом 200
ctx.status(200),
);
}),
// ...
];
// browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers.js';
// Создаем Service Worker с переданными обработчиками запросов
export const worker = setupWorker(...handlers);
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.jsx';
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./browser.js');
worker.start();
}
ReactDOM.render(<App />, document.getElementById('root'));
- Вы можете использовать его для разработки и отладки
- Поддержка REST API и GraphQL
- Выполнение на стороне клиента
- Поддержка TypeScript
- Независимый от фреймворков
rest.post('/login', (req, res, ctx) => {
if (req.body.username === 'real-user') {
// возвращаем ответ
// только когда `username` имеет нужное значение
return;
}
const { authToken } = req.cookies;
if (isValidToken(authToken)) {
return res(
ctx.json({id: 'abc-123', firstName: 'John'}),
)
}
return res(
ctx.status(403),
ctx.json({ message: 'Failed to authenticate!' }),
)
});
Исправление ответов сервера
rest.get('https://api.github.com/users/:username', async (req, res, ctx) => {
// Исходный запрос к URL, получаем ответ
const originalResponse = await ctx.fetch(req);
const originalResponseData = await originalResponse.json();
return res(
ctx.json({
location: originalResponseData.location,
firstName: 'Not the real first name',
}),
);
});
const handler = rest.get('/books', (req, res, ctx) => {
return res(ctx.json({ title: 'The Lord of the Rings' }));
});
const worker = setupWorker(handler);
worker.start({
onUnhandledRequest(req) {
console.error(
'Found an unhandled %s request to %s',
req.method,
req.url.href,
)
},
});
Page Object Pattern
- много тестов
- много кода в тестах
- сложно понимать структуру и флоу
Page objects
- упрощение разработки
- упрощение поддержки
- Высокоуровневый API
DRY
-принцип: создаем переиспользуемый код избегая повторов
- page object — обертка над
HTML
страницей или ее частью - Ее API специфично для конкретного приложения
Манипулируйте элементами страницы, не углубляясь в
HTML
findElementWithClass('album')
=>selectAlbumWithTitle()
findElementWithClass('rating').setText(5)
=>updateRating(5)
class SearchPage {
constructor(page) {
this.page = page;
}
async navigate() {
await this.page.goto('https://mail.ru');
}
async search(text) {
await this.page.fill('[data-testid="search-input"]', text);
await this.page.press('[data-testid="search-button"]', 'Enter');
}
}
import SearchPage from './models/search.js';
test("search", () => {
const page = await browser.newPage();
const searchPage = new SearchPage(page);
await searchPage.navigate();
await searchPage.search('search query');
// ...
})
Преимущества
- Если UI страницы изменится
- тесты изменять не надо
- нужно изменить код в page object
- Все изменения касающиеся поддержки этого нового UI находятся в одном месте
- Все доступные операции или сервисы на странице хранятся в одном месте вместо того, чтобы дублироваться во всех тестах
- Код тестов легче понять
- Существует четкое разделение между кодом тестов и кодом, относящимся к html-странице, вроде селекторов и верстки
Selenium
test("login", () => {
// заполняем данные на странице входа
driver.findElement(By.name("username")).sendKeys("testUser");
driver.findElement(By.name("password")).sendKeys("testPassword");
driver.findElement(By.name("sign-in")).click();
// проверяем, что появляется тег h1 с текстом "Hello testUser" после входа
driver.findElement(By.tagName("h1")).isDisplayed();
expect(driver.findElement(By.tagName("h1")).getText()).toBe("Hello testUser");
}
class SignInPage {
protected WebDriver driver;
// <input name="user_name" type="text" value="">
private usernameBy: By = By.name("username");
// <input name="password" type="password" value="">
private passwordBy: By = By.name("password");
// <input name="sign_in" type="submit" value="SignIn">
private signinBy: By = By.name("sign-in");
constructor(dirver) {
this.driver = driver;
}
loginValidUser(userName: string, password: string): HomePage {
driver.findElement(usernameBy).sendKeys(userName);
driver.findElement(passwordBy).sendKeys(password);
driver.findElement(signinBy).click();
return new HomePage(driver);
}
}
public class HomePage {
protected driver: WebDriver;
// <h1>Hello userName</h1>
private By messageBy = By.tagName("h1");
constructor(driver: WebDriver) {
this.driver = driver;
if (!driver.getTitle().equals("Home Page of logged in user")) {
throw new Error(`This is not Home Page of logged in user, current page is: ${driver.getCurrentUrl()}`);
}
}
getMessageText(): string {
return driver.findElement(messageBy).getText();
}
}
test("login", () => {
const signInPage: SignInPage = new SignInPage(driver);
const homePage: HomePage = signInPage.loginValidUser("userName", "password");
expect(homePage.getMessageText()).toBe("Hello userName"));
});
Правила
- Сами page object никогда не должны ничего тестировать
- Page object содержит представление страницы
- Никакой код, связанный с тем, что тестируется, не должен находиться внутри объекта страницы
- Исключение: убедитесь, что страница отображена верно
- Доступные методы представляют операции, возможные на страницы
Try not to expose the internals of the page
Не создавайте объект для всей страницы, только значимые элементы
- верхний и нижний колонтитулы, список пользователей, логин и т.Д
Общедоступные методы представляют услуги, предлагаемые на странице
Старайтесь не показывать внутренности страницы
Если поведение должно отличаться
class LoginPage {
public HomePage loginAs(username: string, password: string) {
// ... здесь логинимся
}
public LoginPage loginAsExpectingError(username: string, password: string) {
// ... здесь неудавшийся логин
}
public getErrorMessage(): string {
// здесь проверяем, верное ли сообщение об ошибке выбрасывается
}
}
Можно наследоваться
class LoginPage extends Page {
get username() {
return $('#username');
}
get password() {
return $('#password');
}
get submitBtn() {
return $('form button[type="submit"]');
}
get flash() {
return $('#flash');
}
get headerLinks() {
return $$('#header a');
}
open() {
super.open('login')
}
submit() {
this.submitBtn.click();
}
}
Минусы e2e
- медленные
- нестабильные
- непредсказуемое поведение
- изменение UI ломает тест
- нельзя посмотреть строчку где тест упал
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.