Тесты на Vue. Часть 1.

Привет. Сегодня посмотрим, как тестировать Vue-компоненты. На самом деле, немного поработав с NodeJs и Vue, а так же посмотрев на то, как ребята работают с React — могу сказать, что тесты там пишутся на одних и тех же фреймворках/библиотеках, так что эта информация будет полезна не только Vue-разработчикам.

Первым делом, как обычно, смотрим мануал — https://ru.vuejs.org/v2/guide/unit-testing.html. Если мы разворачиваем проект через Vue CLI, то инструменты тестирования у нас уже есть. Нам говорят, что тестировать мы можем посредством двух инструментов — jest и mocha ̶ ̶и̶ ̶н̶и̶ч̶е̶г̶о̶ ̶с̶м̶е̶ш̶н̶о̶г̶о̶!̶ В этой статье мы рассмотрим, как тестировать vue-компоненты с помощью jest. Туториал на vue.org весьма ужат, поэтому я предлагаю разобрать вот этот ман, а потом уже приступить к тестированию нашего компонента. Открывай мануал, параллельно эту статью и поехали.

Для начала клонируем проект, который предлагает нам автор для тестов:

git clone git@github.com:lex111/jest-vue-example.git

Теперь устанавливаем зависимости:

cd jest-vue-example && npm install

В статье по написанию vue-autoselect-компонента мы работали с yarn и, если ты хочешь продолжать работать с ним, то в дальнейшем заменяй npm install на yarn install, а в случае установки пакетов npm install на yarn add, npm install --save-dev на yarn add --dev.

Запускаем dev-сервер:

npm run dev 

Или

yarn run serve

На выходе видим следующий интерфейс:

Далее читаем всё до заголовка «JEST». Если что-то непонятно или с чем-то есть вопросы — пиши в комменты, обсудим.

Устанавливаем jest:

npm install --save-dev jest

Автор говорит, что нам нужно скомпилировать наши .vue файлы в js файлы перед тем, как запускать тесты. Сам jest делать это не умеет, и чтобы его научить нужно добавить в package.json следующие строки:

"jest": {
    "moduleFileExtensions": [
        "js",
        "json",
        "vue"
    ],
    "transform": {
        "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
        ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    }
}

Что, к слову, в склонированном проекте уже сделано. Читаем дальнейший абзац и ставим пакеты:

npm install --save-dev babel-jest vue-jest

Автор говорит, что в директории src/components требуется создать каталог __test__, после чего в него добавить файл MessageToogle.spec.js. Если посмотреть на склонированный проект — все тесты там уже описаны, поэтому для чистоты эксперимента я предлагаю дропнуть из __test__ все файлы и создать чистый файл MessageToogle.spec.js.

А вот дальше, я думаю, стоит дать пояснения некоторые пояснения касательно кода и тестовых файлов.

Возникает первый вопрос — что за расширение — .spec.js? Это расширение для тестовых файлов, причём имя файла должно называться так же, как и сам модуль — в нашем случае MessageToogle.

    import Vue from 'vue'
    import MessageToggle from '@/components/MessageToggle.vue'

   describe('MessageToggle.vue', () => {
      it('displays default message', () => {
        const Ctor = Vue.extend(MessageToggle)
        const vm = new Ctor().$mount()

        expect(vm.$el.textContent).toContain('default message')
      })
})

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

describe имеет большой набор API, с которыми мы познакомимся в процессе.

Каждый тест в блоке describe начинается к ключевого слова it. Если посмотреть в документацию, то про it мы там мало что найдём, потому что it — это алиас для ключевого слова test, о котором, в свою очередь, в документации много чего сказано. Важно понимать, что it-методы — это и есть тесты. В документации есть прекрасный пример, который я нагло продублирую — допустим, нам нужен тест, который проверяет, что метод вернёт 0:

test('did not rain', () => {
  expect(inchesOfRain()).toBe(0);
});

Первый аргумент, как я уже говорил, — заголовок теста, он будет выведен в консоли. Второй — callback-метод, который выполняет сам тест. В этом методе мы видим ещё один важный элемент — метод expect — аналог утверждений, о которых мы говорили в предыдущих статьях. expect можно расценивать как ожидание нами какого-то результата, собственно, документация и переводит этот метод как «ожидание». В примере выше мы ожидаем, что функция inchesOfRain вернёт 0. Как ты уже мог догадаться, ожидай большое количество и ознакомится с ними можно по этой ссылке.

А теперь вернёмся к мануалу. Мы остановились на этом примере:

    import Vue from 'vue'
    import MessageToggle from '@/components/MessageToggle.vue'

   describe('MessageToggle.vue', () => {
      it('displays default message', () => {
        const Ctor = Vue.extend(MessageToggle)
        const vm = new Ctor().$mount()

        expect(vm.$el.textContent).toContain('default message')
      })
})

Теперь можно его разобрать:

Описывается группа текстов с заголовком MessageToggle.vue.

В группе всего один тест, определяемый в callback-функции метода it с названием displays default message. Дальше идут 2 строчки, с методами которых мы познакомимся чуть позже, а пока просто кратко попытаюсь их объяснить:

Vue.extend создаёт подкласс, принимающий параметры компонента в аргументе, а если проще — создаёт класс, позволяющий в дальнейшем создать наш компонент через обычный вызов конструктора.

Второй строкой мы создаём компонент и монтируем его с помощью метода $mount. $mount — это хук жизненного цикла, который «встраивает» в DOM-дерево наш компонент. Звучит сложно и непонятно, но в следующих материалах мы подробно с этим поработаем 🙂

И, наконец, автор создаёт ожидание через expect. В качестве аргумента, как обычно, идёт то, что мы хотим проверить. $el — это свойство уже вмонтированного в DOM-дерево компонента, позволяющее дёргать DOM-элементы. Когда мы обратились к какому-либо DOM-элементу, будь то <p>, <button> или что-то другое, мы можем использовать стандартное js-api. В примере автор использует свойство textContent, которое возвращает (или позволяет задать) текстовое содержимое элемента или его потомков. Тем самым в качестве аргумента мы передаём текст, описанный внутри компонента MessageToggle. Откроем компонент MessageToggle.vue:

<template>
 <div>
  <p>{{msg}}</p>
  <button id="toggle-message" @click="toggleMessage">
   Change message
  </button>
 </div>
</template>

<script>
  export default {
    name: 'message-toggle',
    data: () => ({
      msg: 'default message'
    }),
    methods: {
      toggleMessage () {
        this.msg = this.msg === 'message' ? 'toggled message' : 'message'
      }
    }
  }
</script>

Видно, что в тег <p> подставляется строка со значением default message. Не трудно догадаться, что метод toContain, в рассматриваемом нами тесте, позволяет проверить, содержится ли переданная в него строка в аргументе, полученном внутри expect. Чтобы убедится, что тест корректно отрабатывает, автор предлагает добавить алиас для команды jest в файл package.json:

"unit": "jest"

Что, кстати, в склонированном проекте уже сделано:

И запустить эту команду:

Всё выполнилось успешно, а значит можешь себя поздравить — первый js-тест пройден 🙂

Едем дальше — автор предлагает установить vue-test-utils — библиотеку, упрощающую процесс написания тестов, что мы и сделаем:

npm install --save-dev vue-test-utils

И заменяем ранее написанный нами тест на следующий:

import { shallowMount } from '@vue/test-utils'
import MessageToggle from '@/components/MessageToggle.vue'

describe('MessageToggle.vue', () => {
  it('displays default message', () => {
    const wrapper = shallowMount(MessageToggle)
    expect(wrapper.text()).toContain('default message')
  })
})

Смотрим, что у нас тут из нового:

  • shallowMount — как говорит документация, аналогично $mount, метод создаёт объект, который содержит примонтированный (читай — встроенный) и отрендеренный vue-компонент, но вместо дочерних компонентов у него заглушки. И это, я считая, круто — нам не нужно рендерить всю вложенную логику, ибо временам и это бывает очень и очень затратно, а проверить нам нужно один конкретный компонент.
  • .text() — вспомогательный метод объекта, который мы вернули выше. Метод возвращает текст, вложенный в компонент. Согласись, это явно удобней и понятней, чем использовать vm.$el.textContent. Методов вроде text() — много, подробней можно посмотреть здесь.

Запускаем:

Успешно 🙂

Результат не изменился, чего и следовало ожидать. Но вот на код мы потратили гораздо меньше времени.

Теперь, чтобы не терять время, мы сразу разберём следующий тест:

import { mount } from '@vue/test-utils'
import MessageToggle from '@/components/MessageToggle.vue'

describe('MessageToggle.vue', () => {
  it('displays default message', () => {
    const wrapper = mount(MessageToggle)
    expect(wrapper.text()).toContain('default message')
  })

  it('toggles message when button is clicked', () => {
    const wrapper = mount(MessageToggle)
    const button = wrapper.find('#toggle-message')
    const p = wrapper.find('p')

    button.trigger('click')

    expect(p.text()).toBe('message')

    button.trigger('click')

    expect(p.text()).toBe('toggled message')
  })
})

Первый it нам знаком, поэтому разбираем второй.

После монтирования компонента мы ищем, с помощью метода find из библиотеки vue-test-utils селектор с id toggle-message и селектор по тегу <p>. Затем мы провоцируем событие click, с помощью метода trigger и проверяем, какой текст содержит тег <p>. Затем вызываем событие второй раз и проверяем текст ещё раз. Если зайти в интерфейс и понажимать на кнопку руками — будет понятно, что текст выводится именно такой

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

Я думал добавить в рамках этой же статьи тестирование нашего компонента, но статья в этом случае получается слишком большой, поэтому протестируем в следующей.

Спасибо за внимание 🙂

Twitter — https://twitter.com/SeniorJun — анонсы, мысли и обсуждения. Тут обычно то, что в канал я не рискну писать 🙂

Поддержать развитие канала — https://www.patreon.com/junsenior

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *