Зарегистрируйтесь, чтобы продолжить обучение

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(driver) {
    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 ломает тест
  • нельзя посмотреть строчку где тест упал

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»