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

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.

Докладніше в документації: Vue Test Utils: початок

Введення в поле - 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» - ні.

Докладніше в документації: Vue Test Utils: обробка подій

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 - це деталь реалізації. Перевіряйте вивід.

Докладніше в документації: Vue Test Utils: передача даних