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 з перевіркою автентифікації) варто тестувати на справжньому маршрутизаторі: перейти на захищений маршрут і перевірити, де опинився користувач.
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) - разом з фабриками й станом бази.
Тест має ламатися, коли ламається поведінка, і не ламатися, коли код переписали без зміни поведінки. Тести, що перевіряють деталі реалізації, роблять навпаки.
Деталі реалізації, яких не варто торкатися:
- внутрішній стан через
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 працюють - це вже протестовано; - дублювання логіки в тесті: обчислювати очікуване значення тим самим алгоритмом, що й код, - тест завжди «зелений», навіть якщо алгоритм хибний. Очікування задають явними значеннями.