Это руководство было написано для Vue.js 3 и Vue Test Utils v2.
Версия для Vue.js 2 здесь.
# Инициирование событий
Обработка пользовательского ввода – одна из самых распространённых задач во Vue-компонентах. vue-test-utils
и Jest позволяют с лёгкостью тестировать ввод данных. Давайте посмотрим, как можно использовать trigger
и моки Jest, чтобы убедиться в правильности работы компонента.
Исходный код для теста можно найти здесь.
# Создание компонента
Мы создадим простой компонент с формой <FormSubmitter>
, содержащий <input>
и <button>
. Когда мы кликаем по кнопке, что-то должно произойти. Сначала будем выводить сообщение об успешной отправке формы, затем разберём более интересные примеры, в которых отправка будет происходить к некоторому endpoint.
Создадим <FormSubmitter>
и добавим следующий шаблон:
<template>
<div>
<form @submit.prevent="handleSubmit">
<input v-model="username" data-username>
<input type="submit">
</form>
<div
class="message"
v-if="submitted"
>
Спасибо за ваше сообщение, {{ username }}.
</div>
</div>
</template>
Когда пользователь отправляет форму, мы показываем благодарственное сообщение. Мы хотим отправлять форму асинхронно: для этого используется @submit.prevent
, который предотвращает обычное поведение формы, а именно – обновление страницы после отправления.
Теперь добавим логику для отправки формы:
<script>
export default {
name: "FormSubmitter",
data() {
return {
username: '',
submitted: false
}
},
methods: {
handleSubmit() {
this.submitted = true
}
}
}
</script>
Всё достаточно просто: мы устанавливаем submitted
в значение true
, когда форма отправлена. После чего появляется <div>
с благодарственным сообщением.
# Написание теста
Давайте посмотрим на тест:
import { mount } from "@vue/test-utils"
import FormSubmitter from "@/components/FormSubmitter.vue"
describe("FormSubmitter", () => {
it("Показывает сообщение после отправки", async () => {
const wrapper = mount(FormSubmitter)
await wrapper.find("[data-username]").setValue("Алиcа")
await wrapper.find("form").trigger("submit.prevent")
expect(wrapper.find(".message").text())
.toBe("Спасибо за ваше сообщение, Алиса.")
})
})
Этот тест достаточно понятен. Мы используем mount
для компонента, устанавливаем значение и применяем trigger
метод из vue-test-utils
, который симулирует пользовательский ввод. trigger
также работает с пользовательскими событиями и с их модификаторами, например, submit.prevent
, keydown.enter
и так далее.
Обратите внимание, когда мы вызываем setValue
и trigger
, мы используем await
. Вот почему нам пришлось пометить тест как async
– чтобы мы могли использовать await
.
setValue
иtrigger
внутри возвращают Vue.nextTick ()
. Начиная с бета-версии 28 vue-test-utils
, вам нужно вызватьnextTick
, чтобы система реактивности Vue обновила DOM. Выполняя await setValue (...)
и await trigger (...)
, вы на самом деле просто используете сокращение для:
wrapper.setValue(...)
await wrapper.vm.$nextTick() // "Ждём пока обновится DOM, перед тем как продолжить тестирование"
Иногда вы можете тестировать, не дожидаясь nextTick
, но если ваши компоненты начинают усложняться, вы можете попасть в состояние гонки, и ваша проверка выполнится до того, как Vue обновит DOM. Вы можете узнать больше об этом в официальной документации vue-test-utils.
Этот тест также следует трём этапам модульного тестирования:
- Предусловие(arrange) – подготовка к тестированию. В нашем случае — это отрисовка компонента.
- Действие(act) – выполнение действий системы.
- Утверждение(assert) – убеждение в соответствии ожидаемого и полученного результата.
Мы разделили каждый этап пустой строкой, что делает наши тесты более понятными.
Запустим тест через yarn test:unit
. Он должен пройти.
Триггер достаточно простой: используем find
, чтобы получить элемент, в котором будем симулировать ввод, затем вызываем trigger
c названием события и модификатором.
# Реальный пример
Формы обычно отправляют к некоему endpoint. Давайте разберёмся, как тестировать компонент с разными реализациями handleSubmit
. Обычной практикой является добавления алиаса Vue.prototype.$http
для вашей HTTP-библиотеки. Это позволяет нам делать ajax-запросы просто вызывая this.$http.get(...)
. Подробнее об этом можно почитать тут
Чаще всего http-библиотекой является axios
, популярный HTTP клиент. В этом случае handleSubmit
выглядел бы примерно так:
handleSubmitAsync() {
return this.$http.get("/api/v1/register", { username: this.username })
.then(() => {
// показываем собщение об успешной отправке
})
.catch(() => {
// обрабатываем ошибки
})
}
В этом случае, одним из способов тестирования является мок для this.$http
, чтобы создать желанную среду для тестирования. Подробнее об опциях global.mocks
можно почитать здесь. Давайте посмотрим на мок http.get
метода:
let url = ''
let data = ''
const mockHttp = {
get: (_url, _data) => {
return new Promise((resolve, reject) => {
url = _url
data = _data
resolve()
})
}
}
Здесь есть несколько интересных вещей:
- мы создаём переменные
url
иdata
, чтобы сохранитьurl
иdata
, переданные в$http.get
. Это позволяет убедиться в том, что запрос достигает своего endpoint c правильными данными. - после присваивания
url
иdata
мы немедленно резолвим промис, тем самым, симулируем успешный ответ от API.
Перед тем, как посмотрим на тест, добавим новую функцию handleSubmitAsync
:
methods: {
handleSubmitAsync() {
return this.$http.get("/api/v1/register", { username: this.username })
.then(() => {
this.submitted = true
})
.catch((e) => {
throw Error("Что-то пошло не так", e)
})
}
}
Также обновим <template>
, используя новый метод handleSubmitAsync
<template>
<div>
<form @submit.prevent="handleSubmitAsync">
<input v-model="username" data-username>
<input type="submit">
</form>
<!-- ... -->
</div>
</template>
Теперь тестируем.
# Мокаем ajax вызов
Сначала, добавим наверху мок реализацию this.$http
, прямо перед блоком describe
:
let url = ''
let data = ''
const mockHttp = {
get: (_url, _data) => {
return new Promise((resolve, reject) => {
url = _url
data = _data
resolve()
})
}
}
Теперь добавим тест, передавая мок $http
в global.mocks
при монтировании:
it("Показывает сообщение после отправки", () => {
const wrapper = mount(FormSubmitter, {
global: {
mocks: {
$http: mockHttp
}
}
})
wrapper.find("[data-username]").setValue("Алиса")
wrapper.find("form").trigger("submit.prevent")
expect(wrapper.find(".message").text())
.toBe("Спасибо за ваше сообщение, Алиса.")
})
Теперь, вместо того, чтобы использовать реальную http-библиотеку, присвоенную в Vue.prototype.$http
, будет взята наша мок реализация. Это хорошо тем, что мы можем контролировать окружение теста и получать одинаковый результат.
Запустив yarn test:unit
, наш тест не пройдёт проверку:
FAIL tests/unit/FormSubmitter.spec.js
● FormSubmitter › Показывает сообщение после отправки
[vue-test-utils]: find did not return .message, cannot call text() on empty Wrapper
Получилось так, что тест завершился перед тем, как вернулся промис от mockHttp
. Мы можем сделать наш тест асинхронным следующим способом:
it("Показывает сообщение после отправки", async () => {
// ...
})
Теперь нам нужно убедиться, что DOM обновлён и все промисы выполнены, прежде чем тест продолжится. await wrapper.setValue (...)
здесь тоже не всегда надёжен, потому что в этом случае мы не ждём, пока Vue обновит DOM, а ожидаем, что внешняя зависимость (в данном случае наш выдуманный HTTP-клиент) зарезолвится.
Один из способов решения данной проблемы – это использование flush-promises, небольшого Node.js модуля, который немедленно резолвит все промисы в режиме ожидания (pending). Установите его с помощью yarn add flush-promises
и обновите тест следующим образом (мы также добавляем await wrapper.setValue (...)
для надёжности):
import flushPromises from "flush-promises"
// ...
it("Показывает сообщение после отправки", async () => {
const wrapper = mount(FormSubmitter, {
global: {
mocks: {
$http: mockHttp
}
}
})
await wrapper.find("[data-username]").setValue("Алиса")
await wrapper.find("form").trigger("submit.prevent")
await flushPromises()
expect(wrapper.find(".message").text())
.toBe("Спасибо за ваше сообщение, Алиса.")
})
Теперь тест проходит проверку. Исходный код flush-promises
занимает около 10 строк, если вам интересен Node.js, то обязательно ознакомьтесь с тем, как это работает.
Нам также нужно убедиться, что endpoint и переданные данные правильные. Добавим ещё две проверки в тест:
// ...
expect(url).toBe("/api/v1/register")
expect(data).toEqual({ username: "Алиса" })
Тест все ещё проходит проверки.
# Заключение
В этой секции мы научились, как:
- использовать
trigger
для событий, даже если используются такие модификаторы, какprevent
- использовать
setValue
, чтобы устанавливать значение для<input>
, который используютv-model
- использовать
await
в паре сtrigger
иsetValue
сawait Vue.nextTick
, чтобы убедиться, что DOM обновился - писать тесты, придерживаясь трёх ступеней модульного тестирования
- мокать методы из
Vue.prototype
, используяglobal.mocks
при монтировании - использовать
flush-promises
, чтобы немедленно резолвить все промисы в режиме ожидания. Полезная техника в модульном тестировании
Исходный код для тестов на этой странице можно найти здесь.