Як ефективно використовувати компонентні тести на Playwright

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Радий вітати, я Олександр Микулич, Frontend Guild Master у TENTENS Tech by SKELAR. У попередньому дописі розповідав про виклики, з якими ми зіткнулись у проєкті, міграцію системи на модульний моноліт та які переваги ми врешті-решт отримали. У цій статті розповім про інший важливий пазл нашої архітектури — компонентні тести на Playwright.

Два роки тому перед нами постав виклик — рефакторинг системи. Тоді ми зрозуміли, що без автотестів далеко не дійдемо. Потрібен механізм, який страхуватиме і дозволить спати спокійніше. Тому почали досліджувати, яке тестування нам потрібно. Зрештою зупинились на компонентних тестах Playwright.

У мережі є багато інформації про те, як працює Playwright всередині, але мало про компонентні тести. Тому сьогодні я сконцентруюсь саме на цьому. Розберемо, як працюють компонентні тести під капотом і наші особливості роботи з ними. Якщо ж вам цікаво більше почитати про Playwright, то можу рекомендувати статтю колеги Олександра про його досвід міграції з Cypress на Playwright.

Приклад компонентного тесту

Пропоную почати з простого прикладу, як може виглядати компонентний тест на Playwright.

Нехай ми маємо vue компонент Counter.vue. При кліку на кнопку «Increment» число збільшується на 1. Якщо ви не знайомі з vue, це не проблема, оскільки компонентне тестування не сильно зав’язане на UI-фреймворк.

 
<script setup lang="ts">
const counter = ref(0)

function inc() {
  counter.value++
}
</script>

<template>
  <div>
    Count: {{ counter }}
    <button @click="inc()">
      Increment
    </button>
  </div>
</template>

Давайте напишемо простенький компонентний тест, який натисне на кнопку і перевірить, що стан системи відповідає очікуванням:

import { test, expect } from '@playwright/experimental-ct-vue'
import Counter from './Counter.vue'

test('should increment ...', async ({ mount }) => {
  const component = await mount(Counter)
  const button = component.locator('button')

  await button.click()

  await expect(component).toContainText('Count: 1')
})

Одна з переваг Playwright полягає у тому, що навіть при базовому знанні js/ts код читається дуже легко. Що ж тут відбувається?

  1. Для початку імпортуємо функції test і expect. Ці функції допоможуть нам описати тест і очікування від системи.
  2. Далі імпортуємо vue-компонент, який потрібно протестувати.
  3. Описуємо тест. Класична структура: підготовка, дія, перевірка (докладніше про це нижче).
  4. Викликаємо функцію mount і передаємо наш компонент. Компонент відмальовується у браузері.
  5. Шукаємо кнопку.
  6. Натискаємо на неї.
  7. Перевіряємо, що стан системи відповідає очікуванням.

І хоч тест виглядає просто, але насправді під капотом все трішки складніше.

Що ж там всередині

Перше, що важливо розуміти, — написаний тест і компонент працюють у різних процесах. Спрощено можна сказати, що тест працює у nodejs-процесі, а код компонента — у браузері. Коли ми запускаємо команду `playwright test` у консолі, запускається runner, який знаходить усі тести у вашому проєкті й певним чином виконує їх.

Процес того, як саме Playwright запускає тести, сьогодні розглядати не будемо, оскільки це не впливає на специфіку компонентних тестів і потягне на окреме обговорення. Та якщо вам буде цікавий розбір цього процесу, напишіть про це у коментарях.

Окремо від ранера буде запущено браузер, з яким Playwright буде надалі комунікувати й робитиме це асинхронно. Ось чому тести на Playwright обсипані await-ами.

Розберемо кожен рядок з нашого тесту

import { test, expect } from ’@playwright/experimental-ct-vue’;

Коли ви пишете звичайний e2e-тест на Playwright, то імпорт цих функцій виглядатиме так:

import { test, expect } from '@playwright/test';.

Чим же відрізняється імпорт з @playwright/test від @playwright/experimental-ct-{react,svelte,vue}?

Ключова різниця тільки в тому, що ви отримуєте додаткову фікстуру `mount` у вашому тесті, яка дозволяє відмалювати потрібний вам компонент на сторінці. Фікстуру mount буде описано нижче.

import Counter from ’./Counter.vue’;

Здавалось би, все просто: імпортуємо компонент vue і далі можемо оперувати змінною Counter, передаючи її у mount. Але насправді, якщо запустити подібний код на nodejs, то ви отримаєте помилку виду «Unknown file extension „.vue“», тому подібний код в ідеалі не мав би навіть запускатись.

Як Playwright це обходить

Це відбувається за допомогою babel і кастомного плагіна «playwright-debug-transform». Задача цього плагіна — обробити код тесту, який буде запущено, і підмінити всі імпорти «*.vue» чи «*.svelte» компонентів на змінні з певною мета-інформацією. Тому на виході замість «import Counter from ‘./Counter.vue’» ми отримаємо такий код:

const Counter = {
  __pw_type: 'importRef',
  id: '..._src_components_Counter_vue',
}

test(’should increment’, async ({ mount }) => {

Тут задаємо ім’я нашого тесту і вказуємо, що потребуємо фікстури mount. Насправді механізм фікстур у Playwright є надзвичайно потужним. У проєкті ми пишемо і підтримуємо свої кастомні фікстури, і це заслуговує на окремий розбір їх підкапотної роботи. Тому, якщо вам цікаво зануритись глибше, також дайте знати про це у коментарях.

const component = await mount(Counter);

Це найцікавіший рядок тесту, оскільки найбільше магії відбувається саме тут.

Але перед тим, як розібратись з mount, розберімося, як код нашого компонента взагалі потрапляє у бразер?

Для цього Playwright робить білд вашого проєкту, використовуючи vite. Щоб vite зміг це зробити, йому потрібна точка входу. Нею виступає index.html та index.ts у папці Playwright.

index.html початково виглядає таким чином:

<!DOCTYPE html>
<html lang="en" >
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.icon" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./index.ts"></script>
  </body>
</html>

index.ts початково — пустий файл. Тут можна імпортувати потрібні нам стилі чи якісь бібліотеки, які знадобляться для роботи додатка. Маючи цю точку входу, vite розуміє, який саме код повинен потрапити у фінальний білд.

Після того, як білд готовий, буде запущено preview-сервер. Зазвичай він буде доступний за адресою: localhost:3100. Відкривши це посилання у браузері, буде завантажено збілджену статику.

Під час білда Playwright робить декілька важливих і неявних речей з «playwright/index.ts»:

  • додає глобальну функцію «playwrightMount» — про неї буде нижче;
  • на етапі підготовки коду тестів з babel, Playwright запам’ятовує усі компоненти, які ми імпортували у наших тестах; надалі він прокидує їх у реєстр компонентів у «playwright/index.ts», щоб потім мати до них доступ у браузері.

Після того, як білд готовий, preview-сервер запущено, Playwright запускає тест, відкриває браузер і переходить на localhost:3100.

Далі розберемо безпосередньо «const component = await mount(Counter);». Для наочності підставимо значення у змінній Counter:

// const component = await mount(Counter)
const component = await mount({
  __pw_type: 'importRef',
  id: '..._src_components_Counter_vue',
})

З коробки Playwright дозволяє нам передати якийсь шматок коду в браузер і виконати його там. Фікстура mount якраз використовує таку можливість:

  • вона отримує метадані компонента;
  • викликає глобальну функцію playwrightMount з цими метаданими вже у браузері;
  • у браузері функція playwrightMount знаходить потрібний компонент у реєстрі компонентів і передає його фреймворку vue на відмалювання;
  • далі браузер повертає селектор відмальованого компонента у фікстуру;
  • фікстура mount зі свого боку створює локатор на цей компонент і повертає його у тест у змінну «component».

Ось і вся підкапотна магія компонентного тестування. Усі наступні кроки тесту є стандартними для Playwright і не містять якоїсь специфіки.

Ось так це виглядатиме разом:

const button = component.locator(’button’);

За допомогою механізму локаторів ми оголошуємо, як саме потрібно шукати нашу кнопку.

await button.click();

На цьому етапі кнопку фактично буде знайдено у браузері, й відбудеться клік. Знову нагадаю про необхідність await, оскільки тест і кнопка зараз перебувають в окремих процесах, і комунікація між ними асинхронна.

await expect(component).toContainText(’Count: 1′);

Знову шукаємо за локатором фактичний елемент у браузері і перевіряємо, чи в DOM-і є потрібний нам текст.

Бонусом пропоную поглянути, як насправді виглядає код нашого тесту при запуску:

import { test, expect } from '@playwright/experimental-ct-vue'
const Counter = {
  __pw_type: 'importRef',
  id: '..._src_components_Counter_vue',
}

try {
  test('should increment ...', async ({ mount }) => {
    const component = await mount(Counter)
    const button = component.locator('button')

    try {
      await button.click()
    } catch (playwrightError) {
      debugger
      throw playwrightError
    }

    try {
      await expect(component).toContainText('Count: 1')
    } catch (playwrightError) {
      debugger
      throw playwrightError
    }
  })
} catch (playwrightError) {
  debugger
  throw playwrightError
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...

Благо, що генеруються source maps і при дебазі у тому ж VSCode ми можемо працювати з нашим оригінальним тестом, навіть не здогадуючись, що під капотом з ним відбулися зміни.

Час виконання

Часто пишуть, що Playwright швидкий. Для нас він перевершив усі очікування.

Зараз ми використовуємо компонентні тести на двох основних репозиторіях. На наших gitlab-ранерах (у Chrome) отримуємо такі результати:

Репозиторій 1. Наш основний продукт зі складною конфігурацією і логікою старту.

  • Кількість тестів: 1046.
  • Час виконання: 6 хв.
  • Кількість воркерів: 16.

Репозиторій 2. Набагато легша конфігурація і простий старт.

  • Кількість тестів: 747.
  • Час виконання: 2 хв.
  • Кількість воркерів: 16.

Ось так виглядають логи тестів:

Наші особливості написання комп. тестів

Уже близько двох років ми використовуємо компонентне тестування і за цей час випрацювали певні домовленості та способи написання таких тестів. Про це далі.

Домовленості

  1. Тест для нас — це першочергово документація роботи компонента, тому читабельність і легкість сприйняття — у пріоритеті.
  2. Структура тесту: A-AA-AA.
  3. Тест не повинен знати про внутрішню реалізацію компонента.

Докладніше по кожному з пунктів.

DRY vs легкість читання

У тестах ми допускаємо порушення принципу DRY на користь легкості сприйняття.

Патерн тестування A-AA-AA (Arange — Act/Assert — Act/Assert)

Arange.

Підготовка до безпосереднього тестування. Сюди входять:

  • Підготовка фейкових даних і мок data-endpoints.
  • Підготовка оточення: desktop/mobile, роль користувача, активні фічі чи A/B тести і т.д.
  • Маунт компонента — await mount (MyComponent).
  • Підготовка локаторів.

Act.

Цей етап передбачає:

  • взаємодію з компонентом (click, text typing і т.д.);
  • непряму взаємодію, наприклад, через оточення, як-от відправка WebSocket-повідомлення і т.п.

Дуже важливо, щоб на цьому етапі ми максимально симулювали поведінку користувача або бекенд-системи. Не використовуємо хаки, які дозволяють напряму викликати метод компонента чи сервісу. Тест не повинен знати про внутрішню реалізацію — він повинен декларувати очікувану бізнес-поведінку.

Assert.

Перевірка, що стан системи на певний момент відповідає очікуванням.

На відміну від стандартної структури в юніт-тестах AAA, ми допускаємо, що в комп. тестах буде повторено етапи «дії» і «перевірки».

Відносини між тестом і компонентом

Тест повинен сприймати компонент, як чорну скриньку, тому він не може знати дечого про компонент.

1. Внутрішню реалізацію, а саме:

  • методи;
  • вотчери (watch);
  • приватні властивості.

2. Сервіси (класи), які використовує компонент для своєї роботи.

Тест може знати:

1. Вхідні дані (props).

2. Вихідні повідомлення (events).

3. Оточення, в якому повинен працювати компонент (mobile, роль користувача і т.д.).

4. End-point-и, на які ходить компонент, і дані, які він відправляє або отримує.

5. Візуальне представлення компонента. Наприклад, тест знає, що компонент має кнопку з певним `data-test-id`.

6. Взаємодія з Browser API. До прикладу, завантаження файлу.

Менеджмент фейкових даних, або network mocking

З коробки Playwright дає змогу зручно мокати запити на бекенд таким чином:

test('...', async ({ page }) => {
  ...
  await page.route('*/**/api/v1/get-user-data', async route => {
    await route.fulfill({
      status: 200,
      json: {
        userName: 'test-user',
        userId: 12345,
        email: '[email protected]',
        role: 'admin',
      },
    })
  })
  ...
})

Але якщо в межах одного тесту у вас є потреба замокати багато ендпоїнтів, а деякі з них ще й на етапі ініціалізації додатка, то такий спосіб виглядає не найкращим. І це був саме наш випадок.

Зараз ми працюємо з даними таким чином:

  1. Описуємо data-файл, який відображає потрібний нам бізнес-сценарій.
  2. За допомогою тега `@data-` підключаємо його до тесту.

Приклад:

  • Файл з даними «userWithZeroBalance.ts».
// data/userWithZeroBalance.ts
export default generateData({
  currentUser: {
    name: 'John Doe',
    email: '[email protected]',
    role: 'user',
  },
  balance: {
    amount: 0,
    currency: 'USD',
  },
})
  • Підключаємо data-файл до тесту тегом
“@data-userWithZeroBalance”.

// .../MyComponent.pw.spec.ts
test('should ... @data-userWithZeroBalance', async ({ mount }) => {
  const component = await mount(MyComponent)
  ...
})

До одного тесту можна підключити декілька файлів одночасно, що дозволяє розбивати дані на певні бізнес-сценарії й підключати їх окремо за потреби.

Є й інші цікаві механіки, які ми використовуємо:

  • Скріншот тестування і скріншот Design Review.

Playwright вміє у тестування скріншотів і ми це використовуємо в багатьох місцях. Додатковим бонусом отримуємо можливість дизайнеру перевірити відповідність розробки до дизайну ще на етапі CodeReview у gitlab.

  • Автоматична перевірка продуктового трекінга.

Наш продукт густо покритий трекінгом, і бувають кейси, коли під час рефакторингу зникає або починає менше відстрілюватись трекінговий івент. Компонентні тести детектять подібні випадки й сигналізують нам про це.

  • Перемотування часу (до речі, в останніх релізах це вже доступно з коробки, і нема потреби реалізовувати самому).

Особливо корисна історія для time-specific фіч. Для прикладу, потрібно показати якийсь попап користувачу за 5 хвилин після того, як він зробив якусь дію в системі.

  • Fullpage testing.

Комп. тести дають можливість протестувати не окремий компонент, а всю сторінку за певним роутом. У деяких випадках це дуже зручно, і приносить багато цінності.

  • Параметризація тестів.

У випадку, якщо потрібно запустити той самий тест, у різних тестових обставинах у Playwright є можливість параметризації тестів.

Приклад звичайного тесту на Playwright:

[
  { name: 'Alice', expected: 'Hello, Alice!' },
  { name: 'Bob', expected: 'Hello, Bob!' },
  { name: 'Charlie', expected: 'Hello, Charlie!' },
].forEach(({ name, expected }) => {
  test(`testing with ${name}`, async ({ page }) => {
    await page.goto(`http://example.com/greet?name=${name}`)

    await expect(page.getByRole('heading')).toHaveText(expected)
  })
})

Ми реалізували схожу можливість, але на основі тегів.

Це виглядає таким чином:

test(` should ...
  [@data-case1, @data-case2, @data-case3]
  [@desktop, @mobile]
`, async ({ mount }) => {
  const component = await mount(MyComponent)
  ...
})

Замість одного тесту відпрацює шість у різних комбінаціях:

  • should ... @data-case1 @desktop
  • should ... @data-case1 @mobile
  • should ... @data-case2 @desktop
  • ...

Чого не вистачає

Зараз у комп. тестах на Playwright є одна бісяча річ, яка сильно уповільнює написання тестів. Якщо змінюєш код компонента і перезапускаєш тести, заново відбувається vite build і стартує preview server. При локальній розробці хочеться працювати з vite dev server, який дозволить моментально перезапускати потрібні тести без необхідності білда. У github playwright є декілька issues на цю тематику, я особисто слідкую за цим.

Але скоро це зміниться. Хоч у документації з цього приводу поки згадок немає, але у коді playwright вже можна знайти логіку, яка дозволяє запускати комп. тести з vite dev server. Я вже спробував цю фічу на одному з наших нових репозиторіїв, і у зв’язці з playwright ui mode це виводить розробку на новий рівень ефективності. І зокрема відкриває такі шляхи, як використання TDD з комп. тестуванням.

Висновки

Playwright — це потужний інструмент у світі автоматизованого тестування. Він потихеньку відвойовує своє заслужене перше місце поміж конкурентів. Ми використовуємо його як для E2E, так і для компонентного тестування.

І за два роки можемо впевнено сказати, що задоволені вибором і досвідом, який надає цей інструмент. Якщо ж говорити про компонентне тестування, то у нашому випадку це ідеальний баланс між швидкістю написання і ціною виконання тестів.

А чи бачите ви сенс у компонентних тестах у себе на проєкті? Напишіть про це у коментарях.

👍ПодобаєтьсяСподобалось20
До обраногоВ обраному12
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Цікавий матеріал. Дякую! Таке питання а чи маєте Ви юніт тести? І чому компоненти не тестуєте на тому рівні?

Дякую! Радий, що було цікаво) Юнітів мізерна кількість і нові не пишемо. Зараз зосередились на тестах, які дають нам найбільше впевненості і при цьому не потребують великої кількості часу на підтримку. Якщо коротко, зараз юніти погано лягають на нашу систему і архітектуру, і не принесуть нам великої цінності.
Рішення, в яку саме автоматизацію рухатись приймали ще два з половиною роки тому і задокументували цей процес, тому якщо будуть цікавити деталі, пишіть 🙌

Дуже сподобалось, дякую за цікавий матеріал!)

Дуже цікавий матеріал. Люблю, коли розповідають в деталях.
І звісно цікаво почитати продовження

Підписатись на коментарі