Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «Тестування»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Pinia. Пакет @pinia/testing дає createTestingPinia - тестову версію стора, що підключається як плагін:

import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'
import { useCartStore } from '@/stores/cart'

const wrapper = mount(CartButton, {
  global: {
    plugins: [
      createTestingPinia({
        createSpy: vi.fn,
        initialState: { cart: { items: [{ id: 1, qty: 2 }] } },
      }),
    ],
  },
})

const cart = useCartStore()   // той самий екземпляр, що й у компоненті

await wrapper.get('button').trigger('click')
expect(cart.add).toHaveBeenCalledWith(1)

Що важливо знати:

  • дії замінено шпигунами за замовчуванням (stubActions: true) - виклик cart.add() реєструється, але не виконується: стан не зміниться. Тест перевіряє, що компонент викликав дію, а логіку дії тестують окремо;
  • щоб дії виконувалися - stubActions: false;
  • initialState задає стан за id стора;
  • геттери можна перевизначити прямо в тесті: cart.total = 999 (тестовий стор це дозволяє) - щоб перевірити відображення без побудови стану;
  • сам стор тестують без компонентів: setActivePinia(createPinia()) у beforeEach і виклики дій напряму.

Vue Router. Два підходи:

1. Справжній маршрутизатор з пам'яттю - найближче до реальності:

import { createRouter, createMemoryHistory } from 'vue-router'

const router = createRouter({ history: createMemoryHistory(), routes })
router.push('/orders/7')
await router.isReady()

const wrapper = mount(OrderPage, { global: { plugins: [router] } })

createMemoryHistory не чіпає URL середовища тестів; кожен тест створює новий маршрутизатор, щоб стан не протікав.

2. Моки - для компонентів, яким потрібні лише useRoute()/useRouter():

vi.mock('vue-router', () => ({
  useRoute: vi.fn(() => ({ params: { id: '7' } })),
  useRouter: vi.fn(() => ({ push: vi.fn() })),
}))

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

RouterLinkStub з Test Utils замінює <RouterLink> без маршрутизатора й дає перевірити to.

Захисників маршрутів (beforeEach з перевіркою автентифікації) варто тестувати на справжньому маршрутизаторі: перейти на захищений маршрут і перевірити, де опинився користувач.

Докладніше в документації: Pinia: тестування

jsdom і happy-dom - імітація DOM у Node.js. Вони швидкі, але не є браузером: немає розкладки (розміри елементів - нулі), IntersectionObserver, ResizeObserver, canvas, справжнього фокуса, прокрутки, CSS-анімацій і :hover. Тести компонентів, що від цього залежать, або неможливі, або перевіряють моки замість поведінки.

Три рівні тестування фронтенду:

1. Компонентні тести в jsdom (Vitest + Vue Test Utils) - більшість тестів. Логіка компонента, рендер за props, події, взаємодія з мокованим API. Мілісекунди на тест.

2. Компонентні тести в браузері - Vitest Browser Mode (через Playwright чи WebdriverIO): ті самі тести компонентів, але в справжньому Chromium/Firefox/WebKit:

// vitest.config.ts (фрагмент); playwright() - з пакета @vitest/browser-playwright
test: {
  browser: {
    enabled: true,
    provider: playwright(),
    instances: [{ browser: 'chromium' }],
  },
}
import { render } from 'vitest-browser-vue'
import { page } from 'vitest/browser'

it('opens the dropdown', async () => {
  render(Dropdown, { props: { items } })
  await page.getByRole('button', { name: 'Меню' }).click()
  await expect.element(page.getByRole('menu')).toBeVisible()
})

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

3. E2E-тести (Playwright, Cypress) - весь застосунок із бекендом: вхід, оформлення замовлення, оплата в тестовому режимі. Перевіряють, що частини працюють разом. Повільні й крихкіші - тому їх мало, лише для критичних сценаріїв.

Розподіл, що працює: багато швидких компонентних тестів, менше браузерних - для поведінки, яку jsdom не відтворює, і кілька e2e на ключові шляхи користувача.

Пошук елементів за роллю й текстом (getByRole('button', { name: 'Зберегти' })) - спільна практика для всіх рівнів: так тести збігаються з тим, як бачить сторінку користувач і зчитувач екрана, і заразом перевіряють доступність.

У Laravel-проєкті e2e-сценарії зручно писати браузерними тестами Pest (на Playwright) - разом з фабриками й станом бази.

Докладніше в документації: Vitest: режим браузера

Тест має ламатися, коли ламається поведінка, і не ламатися, коли код переписали без зміни поведінки. Тести, що перевіряють деталі реалізації, роблять навпаки.

Деталі реалізації, яких не варто торкатися:

  • внутрішній стан через wrapper.vm: expect(wrapper.vm.isOpen).toBe(true). Перейменували змінну чи перенесли стан у composable - тест впав, хоча все працює. Перевіряйте, що меню видно;
  • виклики приватних методів: «після кліку викликався handleClick» - а не «після кліку з'явилось повідомлення»;
  • структура дочірніх компонентів і CSS-класи, що відповідають за вигляд;
  • кількість рендерів і порядок внутрішніх викликів, якщо це не вимога продуктивності.

Що тестувати:

  • вхід: props, введення й дії користувача, відповіді API, стан сторів;
  • вихід: що відрендерено, які події випромінено, які запити й дії стора викликано, куди перейшов маршрутизатор.

Тести-знімки (snapshots) зберігають HTML компонента у файл і порівнюють з ним при наступних запусках:

expect(wrapper.html()).toMatchSnapshot()

Чим вони небезпечні:

  • падають від будь-якої зміни - додали клас, змінили текст, поміняли порядок атрибутів. Після кількох таких падінь команда звикає оновлювати знімки не дивлячись (vitest -u) - і знімок більше нічого не перевіряє;
  • не кажуть, що саме важливо: у знімку на 200 рядків незрозуміло, яка частина - вимога, а яка - випадковість;
  • великі знімки ніхто не рецензує в пул-реквестах.

Коли знімки доречні:

  • маленькі й стабільні результати: відформатований рядок, згенерований фрагмент розмітки;
  • вбудовані знімки (toMatchInlineSnapshot) - лежать прямо в тесті й видні при рецензії;
  • як тимчасова страховка перед великим рефакторингом.

Ще кілька пасток:

  • покриття заради покриття: 100% рядків не означає, що перевірено важливе. Краще 70% з тестами граничних випадків і помилок;
  • тестування бібліотек: не потрібно перевіряти, що v-model чи Vue Router працюють - це вже протестовано;
  • дублювання логіки в тесті: обчислювати очікуване значення тим самим алгоритмом, що й код, - тест завжди «зелений», навіть якщо алгоритм хибний. Очікування задають явними значеннями.

Докладніше в документації: Тестування у Vue