**Vue Test Utils (VTU)** — официальная библиотека для тестирования Vue компонентов. Используется совместно с тест-раннером:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'jsdom', // симуляция браузера
globals: true,
}
})import { mount, shallowMount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'
// mount — полный рендер с дочерними компонентами
const wrapper = mount(MyComponent, {
props: { title: 'Привет', count: 5 },
})
// shallowMount — дочерние компоненты заменяются заглушками
// Быстрее, изолирует тестируемый компонент
const wrapper = shallowMount(MyComponent, {
props: { title: 'Привет' },
})import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
describe('UserCard', () => {
it('отображает имя пользователя', () => {
const wrapper = mount(UserCard, {
props: { name: 'Иван', age: 25 },
})
expect(wrapper.text()).toContain('Иван')
expect(wrapper.find('.age').text()).toBe('25')
})
it('скрывает кнопку если disabled=true', () => {
const wrapper = mount(UserCard, {
props: { name: 'Иван', disabled: true },
})
expect(wrapper.find('button').exists()).toBe(false)
})
})it('эмитирует submit с данными формы', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('[data-test="email"]').setValue('user@example.com')
await wrapper.find('[data-test="password"]').setValue('secret')
await wrapper.find('form').trigger('submit')
const emitted = wrapper.emitted('submit')
expect(emitted).toHaveLength(1)
expect(emitted[0][0]).toEqual({
email: 'user@example.com',
password: 'secret',
})
})// Мокируем useAuth внутри компонента
vi.mock('@/composables/useAuth', () => ({
useAuth: () => ({
user: ref({ name: 'Тестовый пользователь' }),
isAuthenticated: ref(true),
logout: vi.fn(),
}),
}))
it('показывает имя залогиненного пользователя', () => {
const wrapper = mount(Header)
expect(wrapper.text()).toContain('Тестовый пользователь')
})it('загружает и отображает данные', async () => {
// Мокируем fetch
vi.spyOn(global, 'fetch').mockResolvedValue({
json: async () => [{ id: 1, name: 'Vue' }],
})
const wrapper = mount(DataList)
// Ждём завершения асинхронных операций
await flushPromises()
expect(wrapper.find('.item').exists()).toBe(true)
expect(wrapper.text()).toContain('Vue')
})Что тестировать:
Что не тестировать:
**Атрибут data-test** — лучше использовать для поиска элементов вместо классов/id:
<button data-test="submit-btn">Отправить</button>wrapper.find('[data-test="submit-btn"]')Мини-фреймворк тестирования компонентов — упрощённый аналог Vue Test Utils
// Реализуем упрощённый тест-фреймворк для Vue-подобных компонентов.
// Это поможет понять, что делает Vue Test Utils под капотом.
// --- Упрощённый "компонент" ---
function defineComponent(options) {
return {
_options: options,
render(props = {}) {
return options.setup(props)
}
}
}
// --- Упрощённый mount / wrapper ---
function mount(component, { props = {} } = {}) {
const emitted = {}
const emit = (event, ...args) => {
if (!emitted[event]) emitted[event] = []
emitted[event].push(args)
}
// Вызываем setup компонента
const instance = component.render({ ...props, emit })
// Виртуальное "дерево" компонента
const tree = instance
return {
// Получить текстовое содержимое
text() {
function extractText(node) {
if (!node) return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(extractText).join(' ')
if (node.children) return extractText(node.children)
return ''
}
return extractText(tree).trim()
},
// Найти элемент (упрощённо — по tag или .class)
find(selector) {
function search(node) {
if (!node) return null
if (Array.isArray(node)) {
for (const child of node) {
const found = search(child)
if (found) return found
}
return null
}
if (typeof node !== 'object') return null
const { tag, props: p = {}, children } = node
const classes = (p.class || '').split(' ')
const matchTag = selector === tag
const matchClass = selector.startsWith('.') && classes.includes(selector.slice(1))
const matchAttr = selector.startsWith('[') && selector.endsWith(']') &&
p[selector.slice(1, -1)] !== undefined
if (matchTag || matchClass || matchAttr) {
return {
node,
exists() { return true },
text() {
if (typeof children === 'string') return children
if (Array.isArray(children)) return children.filter(c => typeof c === 'string').join('')
return ''
},
trigger(event) {
const handler = p[`on${event[0].toUpperCase() + event.slice(1)}`]
if (handler) handler()
}
}
}
return search(children)
}
const found = search(tree)
return found || {
exists() { return false },
text() { return '' },
trigger() {}
}
},
// Получить эмитированные события
emitted(event) {
return emitted[event] || null
},
// Получить props
props() { return props },
}
}
// --- "Компоненты" для тестирования ---
const UserCard = defineComponent({
setup({ name, age, disabled, emit }) {
return {
tag: 'div',
props: { class: 'user-card' },
children: [
{ tag: 'h2', props: { class: 'name' }, children: name },
{ tag: 'span', props: { class: 'age' }, children: String(age) },
...(disabled ? [] : [{
tag: 'button',
props: { onClick: () => emit('action', { name }) },
children: 'Действие',
}]),
]
}
}
})
// --- Тесты ---
function describe(label, fn) {
console.log(`\n📋 ${label}`)
fn()
}
function it(label, fn) {
try {
fn()
console.log(` ✅ ${label}`)
} catch(e) {
console.log(` ❌ ${label}: ${e.message}`)
}
}
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected)
throw new Error(`Ожидалось ${JSON.stringify(expected)}, получено ${JSON.stringify(actual)}`)
},
toContain(expected) {
if (!String(actual).includes(String(expected)))
throw new Error(`"${actual}" не содержит "${expected}"`)
},
toHaveLength(n) {
if (actual.length !== n)
throw new Error(`Ожидалась длина ${n}, получено ${actual.length}`)
},
toBeNull() {
if (actual !== null) throw new Error(`Ожидалось null, получено ${JSON.stringify(actual)}`)
},
not: {
toBeNull() {
if (actual === null) throw new Error('Ожидалось не null')
},
toBe(expected) {
if (actual === expected)
throw new Error(`Ожидалось НЕ ${JSON.stringify(expected)}`)
},
}
}
}
describe('UserCard', () => {
it('отображает имя', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25 } })
expect(wrapper.text()).toContain('Иван')
})
it('отображает возраст', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 30 } })
expect(wrapper.find('.age').text()).toBe('30')
})
it('показывает кнопку когда не disabled', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25, disabled: false } })
expect(wrapper.find('button').exists()).toBe(true)
})
it('скрывает кнопку при disabled=true', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25, disabled: true } })
expect(wrapper.find('button').exists()).toBe(false)
})
it('эмитирует action при клике', () => {
const wrapper = mount(UserCard, { props: { name: 'Пётр', age: 20 } })
wrapper.find('button').trigger('click')
const emitted = wrapper.emitted('action')
expect(emitted).not.toBeNull()
expect(emitted).toHaveLength(1)
})
})
**Vue Test Utils (VTU)** — официальная библиотека для тестирования Vue компонентов. Используется совместно с тест-раннером:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'jsdom', // симуляция браузера
globals: true,
}
})import { mount, shallowMount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'
// mount — полный рендер с дочерними компонентами
const wrapper = mount(MyComponent, {
props: { title: 'Привет', count: 5 },
})
// shallowMount — дочерние компоненты заменяются заглушками
// Быстрее, изолирует тестируемый компонент
const wrapper = shallowMount(MyComponent, {
props: { title: 'Привет' },
})import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
describe('UserCard', () => {
it('отображает имя пользователя', () => {
const wrapper = mount(UserCard, {
props: { name: 'Иван', age: 25 },
})
expect(wrapper.text()).toContain('Иван')
expect(wrapper.find('.age').text()).toBe('25')
})
it('скрывает кнопку если disabled=true', () => {
const wrapper = mount(UserCard, {
props: { name: 'Иван', disabled: true },
})
expect(wrapper.find('button').exists()).toBe(false)
})
})it('эмитирует submit с данными формы', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('[data-test="email"]').setValue('user@example.com')
await wrapper.find('[data-test="password"]').setValue('secret')
await wrapper.find('form').trigger('submit')
const emitted = wrapper.emitted('submit')
expect(emitted).toHaveLength(1)
expect(emitted[0][0]).toEqual({
email: 'user@example.com',
password: 'secret',
})
})// Мокируем useAuth внутри компонента
vi.mock('@/composables/useAuth', () => ({
useAuth: () => ({
user: ref({ name: 'Тестовый пользователь' }),
isAuthenticated: ref(true),
logout: vi.fn(),
}),
}))
it('показывает имя залогиненного пользователя', () => {
const wrapper = mount(Header)
expect(wrapper.text()).toContain('Тестовый пользователь')
})it('загружает и отображает данные', async () => {
// Мокируем fetch
vi.spyOn(global, 'fetch').mockResolvedValue({
json: async () => [{ id: 1, name: 'Vue' }],
})
const wrapper = mount(DataList)
// Ждём завершения асинхронных операций
await flushPromises()
expect(wrapper.find('.item').exists()).toBe(true)
expect(wrapper.text()).toContain('Vue')
})Что тестировать:
Что не тестировать:
**Атрибут data-test** — лучше использовать для поиска элементов вместо классов/id:
<button data-test="submit-btn">Отправить</button>wrapper.find('[data-test="submit-btn"]')Мини-фреймворк тестирования компонентов — упрощённый аналог Vue Test Utils
// Реализуем упрощённый тест-фреймворк для Vue-подобных компонентов.
// Это поможет понять, что делает Vue Test Utils под капотом.
// --- Упрощённый "компонент" ---
function defineComponent(options) {
return {
_options: options,
render(props = {}) {
return options.setup(props)
}
}
}
// --- Упрощённый mount / wrapper ---
function mount(component, { props = {} } = {}) {
const emitted = {}
const emit = (event, ...args) => {
if (!emitted[event]) emitted[event] = []
emitted[event].push(args)
}
// Вызываем setup компонента
const instance = component.render({ ...props, emit })
// Виртуальное "дерево" компонента
const tree = instance
return {
// Получить текстовое содержимое
text() {
function extractText(node) {
if (!node) return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(extractText).join(' ')
if (node.children) return extractText(node.children)
return ''
}
return extractText(tree).trim()
},
// Найти элемент (упрощённо — по tag или .class)
find(selector) {
function search(node) {
if (!node) return null
if (Array.isArray(node)) {
for (const child of node) {
const found = search(child)
if (found) return found
}
return null
}
if (typeof node !== 'object') return null
const { tag, props: p = {}, children } = node
const classes = (p.class || '').split(' ')
const matchTag = selector === tag
const matchClass = selector.startsWith('.') && classes.includes(selector.slice(1))
const matchAttr = selector.startsWith('[') && selector.endsWith(']') &&
p[selector.slice(1, -1)] !== undefined
if (matchTag || matchClass || matchAttr) {
return {
node,
exists() { return true },
text() {
if (typeof children === 'string') return children
if (Array.isArray(children)) return children.filter(c => typeof c === 'string').join('')
return ''
},
trigger(event) {
const handler = p[`on${event[0].toUpperCase() + event.slice(1)}`]
if (handler) handler()
}
}
}
return search(children)
}
const found = search(tree)
return found || {
exists() { return false },
text() { return '' },
trigger() {}
}
},
// Получить эмитированные события
emitted(event) {
return emitted[event] || null
},
// Получить props
props() { return props },
}
}
// --- "Компоненты" для тестирования ---
const UserCard = defineComponent({
setup({ name, age, disabled, emit }) {
return {
tag: 'div',
props: { class: 'user-card' },
children: [
{ tag: 'h2', props: { class: 'name' }, children: name },
{ tag: 'span', props: { class: 'age' }, children: String(age) },
...(disabled ? [] : [{
tag: 'button',
props: { onClick: () => emit('action', { name }) },
children: 'Действие',
}]),
]
}
}
})
// --- Тесты ---
function describe(label, fn) {
console.log(`\n📋 ${label}`)
fn()
}
function it(label, fn) {
try {
fn()
console.log(` ✅ ${label}`)
} catch(e) {
console.log(` ❌ ${label}: ${e.message}`)
}
}
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected)
throw new Error(`Ожидалось ${JSON.stringify(expected)}, получено ${JSON.stringify(actual)}`)
},
toContain(expected) {
if (!String(actual).includes(String(expected)))
throw new Error(`"${actual}" не содержит "${expected}"`)
},
toHaveLength(n) {
if (actual.length !== n)
throw new Error(`Ожидалась длина ${n}, получено ${actual.length}`)
},
toBeNull() {
if (actual !== null) throw new Error(`Ожидалось null, получено ${JSON.stringify(actual)}`)
},
not: {
toBeNull() {
if (actual === null) throw new Error('Ожидалось не null')
},
toBe(expected) {
if (actual === expected)
throw new Error(`Ожидалось НЕ ${JSON.stringify(expected)}`)
},
}
}
}
describe('UserCard', () => {
it('отображает имя', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25 } })
expect(wrapper.text()).toContain('Иван')
})
it('отображает возраст', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 30 } })
expect(wrapper.find('.age').text()).toBe('30')
})
it('показывает кнопку когда не disabled', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25, disabled: false } })
expect(wrapper.find('button').exists()).toBe(true)
})
it('скрывает кнопку при disabled=true', () => {
const wrapper = mount(UserCard, { props: { name: 'Иван', age: 25, disabled: true } })
expect(wrapper.find('button').exists()).toBe(false)
})
it('эмитирует action при клике', () => {
const wrapper = mount(UserCard, { props: { name: 'Пётр', age: 20 } })
wrapper.find('button').trigger('click')
const emitted = wrapper.emitted('action')
expect(emitted).not.toBeNull()
expect(emitted).toHaveLength(1)
})
})
Реализуй функцию `createTestWrapper(component, { props })`, где component — объект с методом `render(props)` возвращающим объект состояния. Wrapper должен иметь методы: `getProp(key)` — возвращает значение prop, `getState(key)` — возвращает значение из состояния компонента, `trigger(action, ...args)` — вызывает метод из состояния (если он есть) и перерисовывает компонент, `hasEmitted(event)` — возвращает true если компонент эмитировал событие, `getEmitted(event)` — возвращает массив всех payload события.
В функции-фабрике сохрани props в замыкании. emit записывает: if (!_emitted[event]) _emitted[event] = []; _emitted[event].push(args). Первый render вызывается при создании wrapper. В trigger: if (typeof _state[action] === "function") { _state[action](...args); } — затем _state = component.render({ ...props, emit }). В getState вернуть _state[key].
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке