Модульное тестирование компонентов Vue


В этой статье рассматривается информация о модульном тестировании только для Vue.js v2 и более ранних версий. Если вы пытаетесь использовать VueJS v3, эта заметка не будет полезной.

Зачем нужно модульное тестирование?

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

Юнит-тестирование — это метод тестирования программного обеспечения, при котором набор программных компонентов или модулей тестируется по отдельности.

Преимущества:

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

Vue Test Utils (VTU)

VTU — это набор утилит, упрощающих тестирование компонентов VueJS. Пакет предоставляет API для монтажа и взаимодействия с компонентами Vue самостоятельно.

Установка

Существуют различные варианты установки VTU. Пожалуйста, обратитесь к официальной документации VTU для получения подробной информации о том, как установить и настроить VTU.

Проекты VueJS уже имеют установленный бандлер для разработки. Поэтому я бы посоветовал при установке не устанавливать разные компиляторы или системы трансформации для тестов и исходного кода. Это только увеличит сложность проекта и зависимость пакетов. Например: если вы используете babel для исходного кода, используйте его и для тестов.

Написание тестов

С помощью VTU мы можем писать тесты, используя describe, it, test. Аналогично, хуки могут быть реализованы под before, beforeEach, after и afterEach. А для утверждений expect также уже в комплекте. Отлично!

import {mount} from "@vue/test-utils"

// Normally a component to be tested is imported from elsewhere
const FabButton = {
  template: "<button type='button' :disabled='disabled'>{{text}}</button>",
  props: ["disabled", "text"]
}

describe("Fab button component", () => {
  describe("when prop 'disabled' is set to 'disabled'", () => {
    it("should be disabled", () => {
      const wrapper = mount(FabButton, {
        propsData: {
          disabled: "disabled",
          text: "My Button"
        }
      })

      // assertions after loading the component
      expect(wrapper.attributes('type').toBe('button'))
      expect(wrapper.attributes('disabled').toBe('disabled'))
      expect(wrapper.text()).toBe("My Button")
    })
  })
})
Вход в полноэкранный режим Выход из полноэкранного режима

Знание того, что тестировать

В наших тестовых файлах может быть задействовано множество логики. Однако не все нужно тестировать во время модульного тестирования.

Не забывайте, что мы пишем тесты только для определенного компонента. Поэтому мы должны тестировать только функции, предоставляемые этим конкретным модулем.

Так нужно ли тестировать каждую функцию компонента?

Для приведенного выше компонента у нас есть два атрибута внутри элемента button, а именно type и disabled. Мы видим, что атрибут type установлен в статическое значение button, а атрибут disabled связан с компонентом prop disabled. Таким образом, мы можем избежать проверки статических атрибутов и проверить только вычисляемые свойства.

it("should be disabled", () => {
  const wrapper = mount(FabButton, {
    propsData: {
      disabled: "disabled",
      text: "My Button"
    }
  })

  // assertions after loading the component
  expect(wrapper.attributes('disabled').toBe('disabled'))
  expect(wrapper.text()).toBe("My Button")
})
Вход в полноэкранный режим Выход из полноэкранного режима

Некоторые моменты, о которых следует помнить:

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

mount и shallowMount

Я в основном использую shallowMount для модульного тестирования, потому что он не беспокоится о других импортированных или внешних компонентах, используемых в тестируемом компоненте. Кроме того, он позволяет нам утверждать реквизиты, предоставляемые этим внешним компонентам. Но если мы хотим протестировать функциональность компонента, мы можем использовать mount вместо него. Например, если нам нужно нажать на какой-то элемент внутри компонента, мы можем выбрать mount вместо shallowMount.

Заглушки

При тестировании я стараюсь как можно чаще ставить заглушки на компонент. Например, если я пишу модульный тест для компонента, который использует какой-то другой компонент, я могу заглушить эту зависимость, а затем протестировать компонент.

При работе с shallowMount иногда я получаю реквизиты или атрибуты, заданные как [Object][Object]. Я не могу разобрать это до объекта и дальнейшие утверждения не могут быть сделаны. Чтобы решить эту проблему, я использую заглушки более точно. Предоставьте фактический компонент для заглушек, а не просто булево значение.

// Incorrect: this may not always work
shallowMount(Component, {
  stubs: {
    // default stub
    FabButton: true
  }
})
Войти в полноэкранный режим Выход из полноэкранного режима
// Correct: stub with the actual component
import { createLocalVue, shallowMount } from '@vue/test-utils'
import FabButton from "@/somelib/component/FabButton"

// if it should be used by vue
localVue.use(FabButton)

shallowMount(Component, {
  localVue,
  stubs: {
    // specific implementation
    'FabButton': FabButton
  }
})
Войти в полноэкранный режим Выход из полноэкранного режима

Моки

Моки очень полезны при модульном тестировании. Наши компоненты обычно используют различные методы из других компонентов, миксинов, библиотек и т.д. Они не входят в сферу нашей ответственности. Поэтому нам необходимо их моделировать.

Мокинг очень прост. Нам нужно запомнить несколько вещей:

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

В jest мокинг выполняется следующим образом:

// mocks a module with an auto-mocked version
// 'factory' and 'options' parameters are optional
jest.mock(moduleName, factory, options)
// mock internal private functions
const myMockFn = jest.fn()
  .mockReturnValue(true) // default return value
  .mockReturnValueOnce(1) // return value for first call
  .mockReturnValueOnce(2) // return value for second call

// 'first call', 'second call', 'default', 'default'
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());

// mock external library
jest.mock('@nextcloud/axios')
// mock external library methods
jest.mock('lodash', () => ({ 
  ...jest.requireActual('lodash'),
  debounce: fn => { fn.cancel = jest.fn(); return fn } 
}))
Войти в полноэкранный режим Выход из полноэкранного режима

Шпионаж

Он создает имитационную функцию, подобную jest.fn, но также записывает вызовы имитационной функции.

По умолчанию jest.spyOn также вызывает метод spied. Но если мы хотим перезаписать исходную функцию, мы можем использовать:

jest.spyOn(object, methodName).mockImplementations(() => customImplementation)
Войти в полноэкранный режим Выйти из полноэкранного режима

Взаимодействие с пользователем

Они хорошо описаны в документации к vue-test-utils.

Некоторые моменты, которые я хотел бы упомянуть:

  • всегда используйте await при выполнении пользовательских взаимодействий
  await wrapper.find('button').trigger('click')
Вход в полноэкранный режим Выход из полноэкранного режима
  • всегда используйте wrapper.vm для доступа к экземпляру компонента
  expect(wrapper.vm.searchResults).toEqual([])
Войти в полноэкранный режим Выйти из полноэкранного режима
  • обязательно используйте wrapper.vm.$nextTick для ожидания завершения асинхронных операций, если это необходимо
  await wrapper.find('button').trigger('click')
  await wrapper.vm.$nextTick()
  expect(wrapper.find('.content').exists()).toBeTruthy()
Вход в полноэкранный режим Выйти из полноэкранного режима
  • не забудьте повторно запросить элементы, состояния которых изменяются после некоторых взаимодействий:

Предположим, есть компонент с кнопкой, которая переключает наличие содержимого в DOM.

  // Incorrect way:
  const content = wrapper.find('.content')
  await wrapper.find('button').trigger('click')
  expect(fab.exists()).toBeTruthy()
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь элемент content запрашивается перед нажатием на кнопку. Поэтому на данном этапе содержимое отсутствует. Теперь, даже если мы нажмем на тумблер, элемент content останется для нас неизменным. Поэтому мы должны повторно запросить этот элемент.

  // Correct way:
  const content = wrapper.find('.content')
  expect(content.exists()).toBeFalsy()
  await wrapper.find('button').trigger('click')
  expect(content.exists()).toBeTruthy()
Вход в полноэкранный режим Выход из полноэкранного режима

Снимки

Снимки очень полезны при написании тестов. Мы можем использовать их для проверки DOM-структуры компонента или любых других данных, таких как объекты, массивы и т.д.

Например, допустим, у нас есть такой компонент:

<template>
  <div class="card">
    <div class="title">{{card.title}}</div>
    <div class="subtitle">{{card.subtitle}}</div>
    <div class="author">{{card.author.username}}</div>
    <div class="actions">
      <button class="delete" :disabled="!card.canDelete()">Delete</button>
      <button class="edit" :disabled="!card.canEdit()">Edit</button>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    card: {
      type: Object, 
      required: true
    }
  }
}
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Было бы немного утомительнее искать и ожидать каждую деталь от компонента.

it('should render the card correctly', () => {
  // mount the component with the card data
  const title = wrapper.find('.title').text()
  const subtitle = wrapper.find('.subtitle').text()
  const author = wrapper.find('.author').text()
  const deleteButton = wrapper.find('button.delete')
  const editButton = wrapper.find('button.edit')
  expect(title).toEqual('Hello World')
  expect(subtitle).toEqual('This is a subtitle')
  expect(author).toEqual('John Doe')
  expect(deleteButton.attributes().disabled).toBeTruthy()
  expect(editButton.attributes().disabled).toBeFalsy()
})
Вход в полноэкранный режим Выход из полноэкранного режима

Это утомительно и трудно поддерживать. Поэтому мы можем использовать моментальные снимки для проверки всей DOM-структуры компонента.

it('should render the card correctly', () => {
  // mount the component with the card data
  const card = wrapper.find('.card')
  expect(card).toMatchSnapshot()
})
Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все. Все данные карты теперь проверены, и это намного проще в обслуживании. Если что-то изменится в компоненте, нам просто нужно будет обновить снимок.

Любопытно, как сохраняются и поддерживаются снимки?

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

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

Для обновления текущего снимка мы можем использовать флаг --updateSnapshot или просто использовать интерактивный режим jest.

jest --updateSnapshot

Это также полезно для assert для больших наборов данных, например:

expect(response).toMatchObject([
  {id: 1, name: 'Rose', color: {r: 255, g: 0, b: 0}},
  {id: 2, name: 'Lily', color: {r: 0, g: 255, b: 0}},
  {id: 3, name: 'Sunflower', color: {r: 0, g: 0, b: 255}}
])
Войти в полноэкранный режим Выход из полноэкранного режима

можно записать как:

expect(response).toMatchSnapshot()
Войти в полноэкранный режим Выйти из полноэкранного режима

Это позволит сохранить объект ответа в виде снимка, и наш тест будет выглядеть более аккуратно. Кроме того, если что-то изменится в ответе, нам просто нужно будет обновить снимок.

Заключительные размышления

В двух словах, модульное тестирование компонентов Vue с помощью Jest и vue-test-utils — это весело. Не пытайтесь добиться 100% покрытия, лучше старайтесь тестировать реальные возможности компонента. Сообщество Vue имеет хорошую документацию и руководства по тестированию компонентов Vue. Таким образом, у вас, скорее всего, будет меньше проблем с тестированием компонентов и обеспечением безопасности.

Оцените статью
Procodings.ru
Добавить комментарий