СТАТЬЯ

Тестирование событий в Vue.js, часть 2

Из Тестирование приложений Vue.js Эдда Йербурга

__________________________________________________________________

Скидка 37% на Тестирование приложений Vue.js. Просто введите код fccyerburgh в поле для кода скидки при оформлении заказа на manning.com.
_______________________________________________________________________

Часть 2 охватывает:

  • Тестирование форм ввода
  • Ограничения jsdom

Продолжая тему тестирования событий в Vue.js, мы продолжим то, о чем говорили в Части 1, и поговорим о тестировании форм ввода и ограничениях jsdom.

Тестирование форм ввода

От контактных форм до форм регистрации и форм входа — формы ввода есть везде! Формы ввода могут содержать много логики для обработки проверки и выполнения действий с входными значениями, и эту логику необходимо протестировать.

В этом разделе вы узнаете, как тестировать формы, написав тесты для компонента формы. В форме есть поле для ввода электронной почты, чтобы пользователи могли ввести свой адрес электронной почты, два переключателя, с помощью которых пользователи могут выбрать, хотят ли они участвовать в конкурсе, и кнопка подписки (рис. 1).

Когда пользователь отправляет форму, компонент должен отправить запрос POST в API с адресом электронной почты, введенным пользователем, и значением переключателей. Для простоты он не будет включать никакой логики проверки.

Спецификации формы таковы:

  • Он должен размещать значение ввода электронной почты при отправке
  • Он должен размещать значения радиокнопок вступить в соревнование при отправке.

Первый тест на запись проверяет, что вы отправляете запрос POST с адресом электронной почты, введенным пользователем. Для этого вам нужно научиться тестировать значения ввода текста.

Тестирование ввода текста

Элементы ввода используются для сбора данных, введенных пользователем. Часто приложения используют эти данные для выполнения действия, например отправки данных во внешний API.

Интересно отметить, что элементы ввода имеют собственное состояние. Различные типы элементов хранят свое состояние в разных свойствах. Входные текстовые элементы управления, такие как text, email и address, сохраняют свое состояние в свойстве value.

Чтобы проверить, правильно ли элементы ввода используют value, вы должны иметь возможность управлять свойством value ввода в своих тестах. Многие люди не понимают, как установить значение формы ввода. Распространенным заблуждением является то, что имитация события keydown со свойством key изменяет значение элемента. Это неправильно. Чтобы изменить свойство value ввода в JavaScript, вам нужно установить свойство value непосредственно в элементе:

document.querySelector('input[type="text"]').value = 'some value'

Когда вы пишете тест, который использует ввод value, вы должны установить value вручную, прежде чем активировать ввод теста:

wrapper.find('input[type="text"]').value = 'Edd'  wrapper.find('input[type="text"]').trigger('change')  expect(wrapper.text()).toContain('Edd')

В Vue принято использовать директиву v-model для создания двусторонней привязки между входными данными value и данными экземпляра компонента. Для связанного значения любые изменения, которые пользователь вносит в значение формы, обновляют значение данных экземпляра компонента, а любые изменения значения свойства экземпляра применяются к входному свойству value (листинг 8).

ИНФОРМАЦИЯ Если вы не знакомы с директивой v-model, вы можете прочитать о ней в документации Vue — https://vuejs.org/v2/api/#v-model.

Листинг 8. Использование v-model для привязки данных

new Vue({
   el: '#app',
   data: {
     message: 'initial message' // #A
   },
   template: '<input type="text" v-model="message" />', // #B
   mounted() {
      setTimeout(() => this.message = '2 seconds', 2000) // #C
   }
 })

#A Начальное значение сообщения
#B Привязать элемент ввода к данным сообщения. Начальное значение элемента ввода — начальное сообщение
#C Обновляет значение элемента ввода до двух секунд.

К сожалению, установка входного свойства value напрямую не обновит связанное значение. Чтобы обновить v-model текстового ввода, вам нужно установить value для элемента, а затем вызвать событие change для элемента, чтобы принудительно обновить привязанное значение. Это связано с реализацией v-модели в ядре Vue и может измениться в будущем. Вместо того, чтобы полагаться на внутреннюю реализацию v-модели, вы можете использовать метод оболочки setValue. Метод setValue устанавливает значение на входе и обновляет связанные данные, чтобы использовать новое значение (листинг 9).

Листинг 9. Обновление значения и значения v-модели ввода в тесте

const wrapper = shallowMount(Form)  const input = wrapper.find('input[type="email"]') // #A  input.setValue('[email protected]') // #B

#A Получить оболочку элемента ввода
#B Установить значение элемента ввода и обновить связанные данные

В тесте, который вы пишете, вам нужно установить значение элемента ввода с помощью setValue, инициировать отправку формы и проверить, что данные отправляются в запросе POST. Чтобы утверждать, что запрос POST отправлен, вам нужно решить, как вы будете делать запрос POST.

Обычный способ выполнения HTTP-запросов — использование библиотеки. Вы будете использовать популярную библиотеку axios. Вы можете отправить запрос POST, используя метод axios post, который принимает URL-адрес и необязательный объект данных в качестве аргументов:

axios.post('https://google.com', { data: 'some data' })

ИНФОРМАЦИЯ axios – это библиотека для создания HTTP-запросов, аналогичная собственному методу fetch. Нет особой причины использовать эту библиотеку вместо другой библиотеки HTTP; вы используете его как пример.

Приложение, над которым вы работаете, уже настроено на использование axios. Он использует библиотеку vue-axios для добавления свойства экземпляра axios Vue (это можно увидеть в src/main.js). Это означает, что вы можете вызывать axios из компонента:

this.axios.post('https://google.com', { data: 'some data' })

Чтобы убедиться, что тест проверяет, что вы вызвали метод axios post, создайте фиктивный объект axios в качестве свойства экземпляра и убедитесь, что он был вызван с правильными аргументами.

Вы можете утверждать, что фиктивная функция Jest была вызвана с правильными аргументами, используя toHaveBeenCalledWith matcher. toHaveBeenCalledWith matcher утверждает, что фиктивная функция была вызвана с переданными аргументами. В этом тесте вы проверяете, что axios post был вызван с правильным URL-адресом и объектом, содержащим свойство электронной почты:

expect(axios.post).toHaveBeenCalledWith(url, {
   email: '[email protected]'
 })

Проблема в том, что когда вы добавляете дополнительные свойства к данным axios в следующем тесте, тест завершается неудачно, потому что объекты-аргументы не равны друг другу. Вы можете проверить этот тест на будущее, используя функцию Jest expect.objectContaining. Этот помощник используется для сопоставления некоторых свойств в объекте данных, а не для проверки точного соответствия объекта (листинг 10).

Листинг 10. Использование objectContaining

const data = expect.objectContaining({
   email: '[email protected]'
 })
 expect(axios.post).toHaveBeenCalledWith(url, data)

Теперь тест всегда проходит, если свойство электронной почты отправляется с правильным значением.

Пришло время добавить тест. Он выглядит довольно большим, но если вы разберете его, перед запуском события отправки потребуется много настроек. Скопируйте код из листинга 11 в src/components/__tests__/Form.spec.js.

Листинг 11. Тестирование макета было вызвано с привязанным к v-модели значением входной формы

test('sends post request with email on submit', () => {
     const axios = { // #A
       post: jest.fn()
     }
     const wrapper = shallowMount(Form, { // #B
       mocks: {
         axios
       }
     })
     const input = wrapper.find('input[type="email"]') // #C
     input.setValue('[email protected]') // #D
     wrapper.find('button').trigger('submit') // #F
     const url = 'http://demo7437963.mockable.io/validate'
     const expectedData = expect.objectContaining({
       email: '[email protected]'
     })
     expect(axios.post).toHaveBeenCalledWith(url, expectedData) // #G
   })

#A Создать фиктивный объект axios со свойством post
#B Поверхностное монтирование формы с фиктивным axios
#CПолучить оболочку элемента ввода электронной почты
#D Установить значение ввода
#FОтправить форму
>#G Утверждает, что axios.post был вызван с правильным значением URL в качестве первого аргумента.

Прежде чем двигаться дальше и запускать тесты, вам нужно обновить предыдущий тест. В настоящее время предыдущий тест выдаст ошибку, потому что компонент формы пытается вызвать axios.post, что равно undefined. На практике это проблема дырявого ведра. Зависимости свойства экземпляра не существует, и вам нужно исправить дыры (смоделировать свойство экземпляра), чтобы избежать ошибок в тесте.

В src/components/__tests__/Form.spec.js замените код для создания оболочки в тесте отправляет форму при отправке формы следующими фрагментами кода:

  const wrapper = shallowMount(Form, {
    mocks: { axios: { post: jest.fn() } }
  })

Теперь обновите компонент кодом из листинга 12.

Листинг 12. Компонент формы

<template>
     <form name="email-form" @submit="onSubmit">
         <input type="email" v-model="email" /> // #A
         <button type="submit">Submit</button>
     </form>
 </template>
  
 <script>
 export default {
   data: () => ({
     email: null
   }),
   methods: {
     onSubmit (event) {
       this.axios.post('http://demo7437963.mockable.io/validate', { // #B
         email: this.email
       })
       this.$emit('form-submitted')
     }
   }
 }
 </script>

#A Привяжите ввод к свойству электронной почты компонента с помощью директивы v-model
#B Вызов axios.post

Вы видели, как написать тест для компонентов, которые используют значение элемента ввода в утверждении. Вы можете использовать setValue для всех элементов ввода, которые используют текстовый элемент управления, например text, textarea и email. Вам нужно использовать другой метод для других типов ввода, таких как переключатели.

Тестирование переключателей

Радиокнопки — это кнопки, которые вы можете выбрать. Вы можете выбрать только одну кнопку из группы переключателей за раз. Тестирование переключателей немного отличается от тестирования элемента ввода текста.

На нашем сайте проходит конкурс! Каждый, кто видит форму регистрации, имеет шанс принять участие в конкурсе. Когда форма будет отправлена, вы отправите выбор пользователей (используя значение переключателя) в запросе POST к API. Я чувствую еще один тест, чтобы написать!

Тестирование радиокнопок аналогично тестированию форм ввода. Вместо внутреннего состояния value внутреннее состояние переключателей — checked. Чтобы изменить выбранный переключатель, вам нужно установить свойство checked для ввода переключателя напрямую (листинг 13).

ИНФОРМАЦИЯ Свойство checked аналогично свойству value. Это состояние переключателя, которое изменяется пользователем, взаимодействующим с переключателем.

Листинг 13. Обновление значения и значения v-модели переключателя, введенного в тесте

const wrapper = shallowMount(Form)      
const radioInput = wrapper.find('input[type="radio"]') // #A      radioInput.element.checked = true // #B

#A Получить оболочку элемента радио-ввода
#B Установить проверенное свойство элемента радио-ввода напрямую

При прямой установке проверенного значения возникает та же проблема, что и при прямой установке значения текстового элемента управления. v-model не обновляется. Вместо этого вы должны использовать метод setChecked:

wrapper.find('input[type="radio"]').setChecked()

Необходимо написать два теста. Первый тест проверяет, что форма по умолчанию отправляет enterCompetition как истину, потому что флажок «да» установлен по умолчанию. Для краткости я не буду показывать вам, как написать этот тест. Тест, который вы напишете, проверяет переключатель «нет», отправляет форму и утверждает, что enterCompetition ложно.

Это большой старый тест, но опять же, это в основном настройка. Добавьте код из листинга 14 в блок describe в src/components/__tests__/Form.spec.js.

Листинг 14. Тестирование вызова компонента с правильными значениями

test('sends post request with enterCompetition checkbox value on submit', () => {
   const axios = {
     post: jest.fn()
   }
   const wrapper = shallowMount(Form, { // #A
     mocks: {
       axios
     }
   })
   const url = 'http://demo7437963.mockable.io/validate'
  
   wrapper.find('input[value="no"]').setChecked() // #B
   wrapper.find('button').trigger('submit') // #C
  
   expect(axios.post).toHaveBeenCalledWith(url, expect.objectContaining({ // #D
     enterCompetition: false
   }))
 })

#A Компонент формы для мелкого монтирования с фиктивным объектом axios
#B Установите флажок "Нет" в качестве флажка
#C Отправить форму
#D Подтвердить, что axios.post был вызван с правильным значением enterCompetition

Чтобы пройти тесты, вам нужно добавить радиовходы и обновить метод onSubmit, чтобы добавить значение enterCompetition к объекту данных, отправленному с помощью axios.post. Добавьте следующие переключатели в блок ‹template› в src/components/Form.vue:

<input
   v-model="enterCompetition"
   value="yes"
   type="radio"
   name="enterCompetition"
 />
 <input
   v-model="enterCompetition"
   value="no"
   type="radio"
   name="enterCompetition"
 />
 Add enterCompetition to the default object:
 data: () => ({
   email: null,
   enterCompetition: 'yes'
 }),

Наконец, обновите вызов axios, чтобы отправить свойство enterCompetition. Тест ожидает логическое значение, но значения переключателей являются строками, и вы можете использовать оператор строгого равенства, чтобы установить enterCompetition как логическое значение:

this.axios.post('http://demo7437963.mockable.io/validate', {
  email: this.email,
  enterCompetition: this.enterCompetition === 'yes'
})

Запустите модульные тесты и посмотрите, как они проходят: npm run test:unit. Вы добавили все возможные тесты для проверки функциональности формы. Я хотел бы добавить еще один тест, чтобы проверить, что отправка формы не вызывает перезагрузку, но использование jsdom делает невозможным запись. Как любой родитель боится птиц и разговоров с пчелами, я всегда опасаюсь ограничений разговоров о жосдоме.

Понимание ограничений jsdom

Чтобы запустить модульные тесты Vue в Node, вам нужно использовать jsdom для имитации среды DOM. В большинстве случаев это прекрасно работает, но иногда вы сталкиваетесь с проблемами из-за нереализованных функций.

В jsdom есть две нереализованные части веб-платформы:

Макет — это вычисление позиций элементов, в результате чего методы DOM, такие как Element.getBoundingClientRects, ведут себя неожиданным образом. В этой статье вы не встретите никаких проблем с этим, но вы можете столкнуться с этим, если используете положение элементов для расчета стиля в своих компонентах.

Другая нереализованная часть — это навигация. В jsdom нет концепции страниц, и вы не можете делать запросы и переходить на другие страницы. Это означает, что события отправки ведут себя не так, как в браузере. В браузере по умолчанию событие отправки делает запрос GET, который вызывает перезагрузку страницы. Это почти никогда не является желательным поведением, и вам нужно написать код, чтобы событие не делало запрос GET для перезагрузки страницы.

В идеале вы должны написать модульный тест, чтобы убедиться, что вы предотвращаете перезагрузку страницы. С jsdom вы не можете сделать это без экстремальных насмешек, которые не стоят потраченного времени.

Вместо написания модульного теста вам нужно написать сквозной тест, чтобы убедиться, что отправка формы не перезагружает страницу. Я не рассказываю о написании сквозных тестов, но пока вы добавите код без теста.

Чтобы остановить перезагрузку страницы, вы можете добавить модификатор события в директиву v-bind. Откройте src/components/Form.vue и добавьте модификатор события .prevent к отправке v-bind:

<form name="email-form" @submit.prevent="onSubmit">

Модификатор вызывает event.preventDefault, который останавливает перезагрузку страницы при отправке.

Две нереализованные части jsdom — это навигация и макет. Важно понимать эти ограничения, и вы можете защититься от них. Когда вы столкнетесь с ограничениями, вместо насмешек вы должны дополнить свои модульные тесты сквозными тестами, которые проверяют функциональность, основанную на нереализованных функциях jsdom.

Теперь, когда вы предотвращаете дефолт, у вас есть полнофункциональная форма. Вы можете открыть сервер разработки и посмотреть: npm run serve. Теперь очевидно, что эта форма далеко не готова для общественного потребления. Он не имеет стиля и невероятно уродлив. Дело в том, что теперь у вас есть набор юнит-тестов, которые проверяют основную функциональность, и вы можете свободно добавлять стили, не замедляясь из-за юнит-тестов.

ПРИМЕЧАНИЕ Чтобы увидеть, как выглядит готовое приложение, перейдите по ссылке https://github.com/eddyerburgh/vue-email-signup-form-application.

Давайте вспомним, что вы узнали из этой статьи.

Сводка

Из этой статьи вы узнали следующее:

  • Вы можете trigger нативных событий DOM с помощью метода триггера оболочки
  • Вы можете проверить, что компонент отвечает на эмитированные события, вызвав $emit для экземпляра дочернего компонента.
  • Вы можете проверить, что компонент сгенерировал пользовательское событие Vue с помощью метода оболочки emitted.
  • jsdom не реализует навигацию или макет

Упражнения

  1. Как вы имитируете нативное событие DOM в тестах?

Ответ: с помощью метода оболочки trigger

2. Как бы вы проверили, что родительский компонент реагирует на событие, генерируемое дочерним компонентом?

Ответ: создав событие для экземпляра дочернего компонента с помощью метода $emit

Если вы хотите узнать больше о книге, загляните в нашу читалку liveBook здесь и посмотрите эту презентацию.

Об авторе:
Эдд Йербург — опытный разработчик JavaScript и основной участник Vue. Он является основным автором официальной тестовой библиотеки Vue и видной фигурой в сообществе тестирования Vue.

Первоначально опубликовано на freecontent.manning.com.