Junior: питання на співбесіді з теми «Тестування»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Стандартний набір для тестування Vue-компонентів:
- Vitest - запускач тестів на основі Vite: та сама конфігурація, що й у проєкті, підтримка
.vue-файлів і TypeScript без додаткових налаштувань; - Vue Test Utils (
@vue/test-utils) - монтує компонент, дає змогу знаходити елементи, взаємодіяти з ними й перевіряти результат; - jsdom чи happy-dom - імітація DOM браузера в Node.js.
npm i -D vitest @vue/test-utils jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: { environment: 'jsdom' },
})
Тест:
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('starts from the given value and increments', async () => {
const wrapper = mount(Counter, { props: { start: 2 } })
expect(wrapper.get('[data-test="count"]').text()).toBe('2')
await wrapper.get('button').trigger('click')
expect(wrapper.get('[data-test="count"]').text()).toBe('3')
})
})
Ключові моменти:
mountрендерить компонент разом з усіма дочірніми;propsпередають значення як від батька; також єslotsіglobal(плагіни, заглушки, provide);triggerповертає Promise -awaitпотрібен, щоб Vue встиг оновити DOM;getкидає помилку, якщо елемент не знайдено (з зрозумілим повідомленням),findповертає порожню обгортку - зручно для перевірки відсутності:expect(wrapper.find('.error').exists()).toBe(false).
Атрибути data-test для пошуку елементів кращі за класи й структуру: стилі й розмітка змінюються, а тест - ні.
Що перевіряти: те, що бачить і робить користувач - текст, видимі елементи, події, виклики API, - а не внутрішні змінні компонента. Тоді рефакторинг не ламає тести.
Запуск: npx vitest (режим спостереження) чи npx vitest run у CI.
Введення в поле - setValue, клік чи інша подія - trigger. Обидва повертають Promise, який завершується після оновлення DOM:
const wrapper = mount(SearchForm)
await wrapper.get('input[name="query"]').setValue('laravel')
await wrapper.get('select').setValue('php')
await wrapper.get('input[type="checkbox"]').setValue(true)
await wrapper.get('form').trigger('submit.prevent')
setValue встановлює значення і генерує відповідну подію (input чи change), тож v-model спрацює як у браузері.
Клавіші й модифікатори:
await wrapper.get('input').trigger('keydown', { key: 'Enter' })
await wrapper.get('input').trigger('keydown.enter')
Перевірка подій компонента - emitted():
await wrapper.get('[data-test="save"]').trigger('click')
expect(wrapper.emitted()).toHaveProperty('save')
expect(wrapper.emitted('save')).toHaveLength(1)
expect(wrapper.emitted('save')![0]).toEqual([{ id: 7, title: 'Новий' }])
emitted('save') - масив викликів, кожен виклик - масив аргументів. Тому для emit('changed', 3) результат [[3]], а не [3].
v-model на власному компоненті перевіряється через подію оновлення:
expect(wrapper.emitted('update:modelValue')!.at(-1)).toEqual(['нове значення'])
Що варто пам'ятати:
awaitпісляtrigger/setValueобов'язковий, якщо далі перевіряється DOM. Без нього тест бачить старий стан;- запити й інші Promise всередині обробника можуть завершитися пізніше за наступний тік рендеру - тоді потрібен
flushPromises(); - вимкнені елементи:
triggerна кнопці зdisabledподію не генерує - це відповідає браузеру і зручно для перевірки, що заблокована кнопка нічого не робить; - події, яких не має бути, перевіряються так само:
expect(wrapper.emitted('save')).toBeUndefined().
Перевіряйте результат, а не виклик методу. Тест «після кліку з'явилось повідомлення й пішла подія» витримає рефакторинг, а «викликався метод onSave» - ні.
Props передають при монтуванні й змінюють пізніше через setProps:
const wrapper = mount(PriceTag, {
props: { amount: 1250, currency: 'UAH' },
})
expect(wrapper.text()).toContain('1 250')
await wrapper.setProps({ amount: 99 })
expect(wrapper.text()).toContain('99')
setProps повертає Promise - після await DOM уже оновлений. Так перевіряють, що компонент правильно реагує на зміну props, а не лише на початкові значення.
Слоти - рядками з розміткою, компонентами чи функціями рендеру:
const wrapper = mount(Card, {
slots: {
default: '<p>Основний вміст</p>',
header: '<h2>Заголовок</h2>',
footer: FooterActions,
},
})
expect(wrapper.get('header').text()).toBe('Заголовок')
expect(wrapper.html()).toContain('Основний вміст')
Scoped slots - шаблон отримує параметри слоту через params:
const wrapper = mount(DataList, {
props: { items: [{ id: 1, name: 'Оля' }] },
slots: {
item: '<span class="name">{{ params.item.name }}</span>',
},
})
expect(wrapper.get('.name').text()).toBe('Оля')
Так перевіряється контракт слоту: які дані компонент передає назовні.
Атрибути, що не є props (class, id, data-*), перевіряються на корені:
const wrapper = mount(BaseButton, { attrs: { class: 'w-full', 'data-test': 'submit' } })
expect(wrapper.classes()).toContain('w-full')
Що варто перевіряти:
- граничні значення props: порожній масив,
null, дуже довгий текст, нуль - саме там живуть баги; - відсутній слот: компонент має коректно рендеритися, якщо необов'язковий слот не передали (і, наприклад, не показувати порожню обгортку);
- заглушка за замовчуванням у слоті показується, коли вміст не передано.
Чого не варто: перевіряти, що prop «дійшов» у внутрішню змінну через wrapper.vm - це деталь реалізації. Перевіряйте вивід.