Руководства

Введение

Настройка

Для быстрого старта работы с Vue Test Utils, клонируйте наш демонстрационный репозиторий с базовыми настройками и установите зависимости:

git clone https://github.com/vuejs/vue-test-utils-getting-started
cd vue-test-utils-getting-started
npm install

Вы увидите, что проект содержит простой компонент counter.js:

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}

Монтирование компонентов

Vue Test Utils тестирует компоненты Vue монтируя их изолированно, создавая моки необходимых входных данных (входные параметры, инъекции и пользовательские события) и выполняя проверки над результатом (результат рендеринга, вызванные пользовательские события).

Примонтированные компоненты возвращаются внутри Wrapper, который предоставляет множество удобных методов для манипулирования, перемещения и различных запросов для экземпляра компонента Vue.

Вы можете создавать wrapper с помощью метода mount. Давайте создадим файл test.js:

// test.js

// Импортируем метод `mount()` из `vue-test-utils`
// и компонент, который хотим протестировать
import { mount } from '@vue/test-utils'
import Counter from './counter'

// Теперь монтируем компонент и у нас появляется wrapper
const wrapper = mount(Counter)

// Вы можете получить доступ к экземпляру Vue через `wrapper.vm`
const vm = wrapper.vm

// Чтобы изучить wrapper подробнее, просто выведите его в консоль
// и ваши приключения с `vue-test-utils` начнутся
console.log(wrapper)

Тестирование отрендеренного HTML компонента

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

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Компонент Counter', () => {
  // Теперь монтируем компонент и получаем wrapper
  const wrapper = mount(Counter)

  it('отображает корректную разметку', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // также легко проверить наличие других элементов
  it('имеет кнопку', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

Теперь запустите тесты командой npm test. Вы должны увидеть, что все тесты проходят успешно.

Симуляция пользовательских действий

Наш счётчик должен увеличивать значение, когда пользователь нажимает кнопку. Чтобы симулировать это поведение, нам необходимо сначала получить кнопку с помощью wrapper.find(), который возвращает wrapper для элемента кнопки. Мы можем симулировать клик с помощью вызова .trigger() на wrapper кнопки:

it('нажатие кнопки должно увеличивать счётчик', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

Что делать с nextTick?

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

Для упрощения работы, vue-test-utils применяет все обновления синхронно, поэтому вам не потребуется использовать Vue.nextTick для ожидания обновления DOM в ваших тестах.

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

Если вам всё ещё нужно использовать nextTick в ваших тестовых файлах, имейте ввиду, что любые ошибки, выброшенные внутри него, могут не быть отловлены вашей программой для запуска тестов, поскольку внутри он реализован на Promise. Существует два подхода исправления этого: либо вы можете установить коллбэк done как глобальный обработчик ошибок Vue в начале теста, либо вы можете вызывать nextTick без аргумента и вернуть его как Promise:

// эта ошибка не будет отловлена
it('ошибка не будет отслеживаться', (done) => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// два следующих теста будут работать как ожидается
it('должен отлавливать ошибку с использованием done', (done) => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('должен отлавливать ошибку с использованием promise', () => {
  return Vue.nextTick()
    .then(function () {
      expect(true).toBe(false)
    })
})

Что дальше

Общие советы

Понимание что тестировать

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

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

Например, для компонента Counter, который при каждом нажатии кнопки будет увеличивать отображаемый счётчик на 1, может тестироваться с помощью симуляции клика и проверке, что в отрендренном результате значение будет увеличено на 1. Тест не заботится о том, каким образом Counter увеличивает значение, он сосредоточен только на входных данных и результате.

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

Эта тема обсуждается более подробно в отличной презентации Matt O'Connell.

Поверхностный рендеринг

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

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

vue-test-utils позволяет вам монтировать компонент без рендеринга его дочерних компонентов (заменяя их заглушками) с помощью метода shallowMount:

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

const wrapper = shallowMount(Component)
wrapper.vm // примонтированный экземпляр Vue

Проверка вызванных событий

Каждая примонтированная обёртка автоматически записывает все события, вызванные на экземпляре Vue. Вы можете получить записанные события с помощью метода wrapper.emitted():

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted()` возвращает указанный объект:
{
  foo: [[], [123]]
}
*/

Затем вы можете добавить проверки на основе этих данных:

// проверка, что событие было вызвано
expect(wrapper.emitted().foo).toBeTruthy()

// проверка, что событие вызывалось определённое число раз
expect(wrapper.emitted().foo.length).toBe(2)

// проверка, что с событием были переданы определённые данные
expect(wrapper.emitted().foo[1]).toEqual([123])

Вы также можете получить массив событий в порядке их вызова с помощью wrapper.emittedByOrder().

Манипулирование состоянием компонента

Вы можете напрямую манипулировать состоянием компонента с помощью методов setData или setProps на обёртке:

wrapper.setData({ count: 10 })

wrapper.setProps({ foo: 'bar' })

Моки входных параметров

Вы можете передать входные параметры в компонент с использованием встроенной во Vue опции propsData:

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

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

Вы также можете обновить входные параметры на уже примонтированном компоненте с помощью метода wrapper.setProps({}).

Полный список опции можно посмотреть в секции настроек монтирования документации.

Добавление глобальных плагинов и примесей

Некоторые из компонентов могут полагаться на функции, добавляемые глобальным плагином или примесью, к примеру vuex и vue-router.

Если вы пишете тесты для компонентов определённого приложения, вы можете настроить одни и те же глобальные плагины и примеси один раз перед началом ваших тестов. Но в некоторых случаях, например при тестировании набора общих компонентов, которые могут использоваться в разных приложениях, гораздо лучше протестировать ваши компоненты в более изолированной конфигурации, без загрязнения глобального конструктора Vue. Мы можем использовать метод createLocalVue для достижения этого:

import { createLocalVue } from '@vue/test-utils'

// создаём расширенный конструктор `Vue`
const localVue = createLocalVue()

// устанавливаем плагины как обычно
localVue.use(MyPlugin)

// передаём `localVue` в настройки монтирования
mount(Component, {
  localVue
})

Обратите внимание, что некоторые плагины, такие как Vue Router, добавляют свойства только для чтения к глобальному конструктору Vue. Это делает невозможным переустановку плагина на конструкторе localVue или добавление моков для этих свойств

Создание моков инъекций

Другая стратегия для инъекции входных параметров — просто создание их моков. Вы можете это сделать с помощью опции mocks:

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

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // добавление мока объекта `$route` в экземпляр Vue
    // перед монтированием компонента
    $route
  }
})

Подмена компонентов

Вы можете переопределить компоненты, зарегистрированные глобально или локально, используя опцию stubs:

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

mount(Component, {
  // Компонент globally-registered-component
  // будет заменяться пустой заглушкой
  stubs: ['globally-registered-component']
})

Работа с маршрутизацией

Поскольку маршрутизация по определению имеет отношение к общей структуре приложения и включает в себя несколько компонентов, её лучше всего тестировать с помощью интеграционных или end-to-end тестов. Для отдельных компонентов, которые используют возможности vue-router, вы можете создать моки с использованием упомянутых выше методов.

Обнаружение стилей

Ваш тест может обнаруживать только встроенные стили при в jsdom.

Тестирование нажатий клавиш, мыши и других событий DOM

Генерация событий

Wrapper предоставляет метод trigger. Его можно использовать для генерации событий DOM.

const wrapper = mount(MyButton)

wrapper.trigger('click')

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

const wrapper = mount(MyComponent)

wrapper.find('button').trigger('click')

Опции

Метод trigger также может опционально принимать объект options. Свойства объекта options добавятся к Event.

Обратите внимание, что цель (target) не может добавлена в объект options.

const wrapper = mount(MyButton)

wrapper.trigger('click', { button: 0 })

Пример тестирования кнопки мыши

Тестируемый компонент

<template>
<div>
  <button class="yes" @click="callYes">Yes</button>
  <button class="no" @click="callNo">No</button>
</div>
</template>
<script>
export default {
  name: 'YesNoComponent',
  props: {
    callMe: {
      type: Function
    }
  },
  methods: {
    callYes() {
      this.callMe('yes')
    },
    callNo() {
      this.callMe('no')
    }
  }
}
</script>

Тест

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

describe('Click event', () => {
  it('Нажатие на кнопке yes вызывает наш метод с аргументом "yes"', () => {
    const spy = sinon.spy()
    const wrapper = mount(YesNoComponent, {
      propsData: {
        callMe: spy
      }
    })
    wrapper.find('button.yes').trigger('click')

    spy.should.have.been.calledWith('yes')
  })
})

Пример тестирования клавиши

Тестируемый компонент

Этот компонент позволяет увеличивать/уменьшать количество с помощью различных клавиш.

<template>
<input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>
<script>
const KEY_DOWN = 40
const KEY_UP = 38
const ESCAPE = 27
const CHAR_A = 65

export default {
  data() {
    return {
      quantity: 0
    }
  },
  methods: {
    increment() {
      this.quantity += 1
    },
    decrement() {
      this.quantity -= 1
    },
    clear() {
      this.quantity = 0
    },
    onKeydown(e) {
      if (e.keyCode === ESCAPE) {
        this.clear()
      }
      if (e.keyCode === KEY_DOWN) {
        this.decrement()
      }
      if (e.keyCode === KEY_UP) {
        this.increment()
      }
      if (e.which === CHAR_A) {
        this.quantity = 13
      }
    }
  },
  watch: {
    quantity: function (newValue) {
      this.$emit('input', newValue)
    }
  }
}
</script>

Тест

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

describe('Тестирование событий клавиш', () => {
  it('Quantity по умолчанию равно нулю', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Клавиша вверх устанавливает quantity равным 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Клавиша вниз уменьшает quantity на 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape устанавливает quantity равным 0', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Магический символ "a" устанавливает quantity равным 13', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown', {
      which: 65
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

Ограничения

Имя модификатора после точки keydown.up преобразуется в keyCode. Это поддерживается для следующих имён:

key name key code
enter 13
esc 27
tab 9
space 32
delete 46
backspace 8
insert 45
up 38
down 40
left 37
right 39
end 35
home 36
pageup 33
pagedown 34

Важно

Vue Test Utils генерирует событие синхронно. Следовательно, Vue.nextTick не требуется.

Чем запускать тесты

Test runner — это программа, которая запускает тесты.

Есть много популярных программ запуска тестов для JavaScript, и vue-test-utils работает с любой из них. Нет зависимости от используемого test runner.

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

  • Jest наиболее функциональный способ запуска тестов. Он меньше всех требует конфигурирования, устанавливает JSDOM по умолчанию, предоставляет встроенные проверки (assertions) и имеет отличный интерфейс для работы через командную строку. Тем не менее, вам понадобится пре-процессор, чтобы иметь возможность импортировать однофайловые компоненты в свои тесты. Мы создали пре-процессор vue-jest, который может обрабатывать наиболее распространённые функции однофайловых компонентов, но в настоящее время он не имеет 100% паритетности функций с vue-loader.

  • mocha-webpack — это обёртка вокруг webpack + Mocha, но с более оптимизированным интерфейсом и режимом отслеживания. Преимущества этой конфигурации в том, что мы можем получить полную поддержку однофайловых компонентов с помощью webpack + vue-loader, но для этого требуется больше настройки.

Браузерное окружение

vue-test-utils полагается на браузерное окружение. Технически вы можете запустить его в реальном браузере, но это не рекомендуется из-за сложности запуска реальных браузеров на разных платформах. Вместо этого мы рекомендуем запускать тесты в Node с виртуальным браузерным окружением, реализуемым с помощью JSDOM.

Jest настраивает JSDOM автоматически. Для других программ запуска тестов вы можете вручную настроить JSDOM для тестов с помощью jsdom-global в записи для ваших тестов:

npm install --save-dev jsdom jsdom-global

// в настройке теста / entry
require('jsdom-global')()

Тестирование однофайловых компонентов

Однофайловые компоненты Vue требуют предварительной компиляции, прежде чем могут быть запущены в Node или в браузере. Существует два рекомендуемых способа выполнения компиляции: с пре-процессором Jest, или непосредственно с помощью webpack.

Пре-процессор vue-jest поддерживает базовую функциональность однофайловых компонентов, но в настоящее время не обрабатывает блоки стилей или пользовательские блоки, которые поддерживаются только в vue-loader. Если вы полагаетесь на эти функции или другие конфигурации, специфичные для webpack, вам нужно будет использовать связку webpack + vue-loader.

Изучите следующие руководства по вариантам настройки:

Дополнительные ресурсы

Тестирование однофайловых компонентов с Jest

Пример проекта для этой конфигурации доступен на GitHub.

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

Установка Jest

Предположим, что вы начинаете с конфигурации, где правильно настроены webpack, vue-loader и Babel — например, развёрнутый шаблон webpack-simple с помощью vue-cli.

Первым делом нам необходимо установить Jest и vue-test-utils:

$ npm install --save-dev jest @vue/test-utils

Затем, необходимо указать псевдоним для запуска тестов в нашем package.json.

// package.json
{
  "scripts": {
    "test": "jest"
  }
}

Обработка однофайловых компонентов с Jest

Чтобы научить Jest как обрабатывать *.vue файлы, нам необходимо установить и настроить пре-процессор vue-jest:

npm install --save-dev vue-jest

Теперь, создадим секцию jest в файле package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // сообщаем Jest что необходимо обрабатывать `*.vue` файлы
      "vue"
    ],
    "transform": {
      // обрабатываем `*.vue` файлы с помощью `vue-jest`
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    }
  }
}

Примечание: vue-jest в настоящее время не поддерживает все возможности vue-loader, например пользовательские блоки и загрузку стилей. Кроме того, некоторые функции, специфичные для webpack, такие как code-splitting, также не поддерживаются. Чтобы использовать их прочитайте руководство по тестированию однофайловых компонентов с Mocha + webpack.

Обработка псевдонимов webpack

Если вы используете псевдонимы в конфигурации webpack, например когда @ ссылается на путь /src, вам также нужно добавить соответствующую конфигурацию для Jest, используя опцию moduleNameMapper:

{
  // ...
  "jest": {
    // ...
    // добавление поддержки псевдонима @ -> src в исходном коде
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}

Конфигурация Babel для Jest

Хотя последние версии Node уже поддерживают большинство функций ES2015, вы всё равно можете использовать синтаксис ES-модулей и stage-x функции в ваших тестах. Для этого нужно установить babel-jest:

npm install --save-dev babel-jest

Затем мы должны сообщить Jest обрабатывать файлы тестов с JavaScript с помощью babel-jest, добавив запись jest.transform в package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // обрабатывать js с помощью `babel-jest`
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    },
    // ...
  }
}

По умолчанию babel-jest автоматически настраивается по установке. Однако, поскольку мы явно добавили преобразование файлов *.vue, нам теперь нужно также настроить babel-jest.

Предполагая использование babel-preset-env с webpack, конфигурация Babel по умолчанию отключает транспиляцию ES-модулей, потому что webpack уже знает как обрабатывать ES-модули. Однако нам нужно включить его для наших тестов, потому что тесты Jest запускаются непосредственно в Node.

Кроме того, мы можем указать babel-preset-env в качестве цели используемую нами версию Node. Это пропустит транспиляцию ненужных функций и ускорит загрузку тестов.

Чтобы применить эти параметры только для тестов, поместите их в отдельную конфигурацию в env.test (это будет автоматически обработано babel-jest).

Пример .babelrc:

{
  "presets": [
    ["env", { "modules": false }]
  ],
  "env": {
    "test": {
      "presets": [
        ["env", { "targets": { "node": "current" }}]
      ]
    }
  }
}

Расположение файлов тестов

По умолчанию Jest будет рекурсивно выбирать все файлы с расширением .spec.js или .test.js во всём проекте. Если это поведение не соответствует вашим потребностям, то возможно изменить testRegex в секции конфигурации в файле package.json.

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

Покрытие кода (Coverage)

Jest может быть использован для генерации отчётов о покрытии кода в нескольких форматах. Ниже приведён простой пример для начала:

Расширьте вашу конфигурацию jest (обычно расположенную в package.json или jest.config.js) с помощью опции collectCoverage, и затем добавьте массив collectCoverageFrom для определения файлов, для которых требуется собирать информацию о покрытии.

{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js,vue}",
      "!**/node_modules/**"
    ]
  }
}

Это включит отчёты о покрытии с использованием стандартных отчётов о покрытии. Вы можете настроить их с помощью опции coverageReporters:

{
  "jest": {
    // ...
    "coverageReporters": ["html", "text-summary"]
  }
}

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

Пример спецификации

Если вы знакомы с Jasmine, то вы должны чувствовать себя как дома с проверочным API Jest:

import { mount } from '@vue/test-utils'
import Component from './component'

describe('Component', () => {
  test('является экземпляром Vue', () => {
    const wrapper = mount(Component)
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

Тестирование моментальными снимками

Когда вы монтируете компонент с помощью Vue Test Utils, вы получаете доступ к корневому элементу HTML. Это можно сохранить в виде моментального снимка для тестирования моментальными снимками в Jest:

test('renders correctly', () => {
  const wrapper = mount(Component)
  expect(wrapper.element).toMatchSnapshot()
})
npm install --save-dev jest-serializer-vue

Затем добавьте конфигурацию в package.json:

{
  // ...
  "jest": {
    // ...
    // serializer for snapshots
    "snapshotSerializers": [
      "jest-serializer-vue"
    ]
  }
}

Ресурсы

Тестирование однофайловых компонентов с Mocha + webpack

Пример проекта для этой конфигурации доступен на GitHub.

Другая стратегия тестирования однофайловых компонентов заключается в компиляции всех наших тестов с помощью webpack, а затем программой для запуска тестов. Преимущество такого подхода заключается в том, что он даёт нам полную поддержку всех функций webpack и vue-loader, поэтому нам не нужно идти на компромиссы в нашем исходном коде.

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

Настройка mocha-webpack

Мы предположим, что вы начинаете с настройки, когда уже есть правильно настроенные webpack, vue-loader и Babel — например используя шаблон webpack-simple, развёрнутый с помощью vue-cli.

Первое, что нужно сделать, это установить тестовые зависимости:

npm install --save-dev @vue/test-utils mocha mocha-webpack

Затем мы должны указать скрипт test в нашем package.json.

// package.json
{
  "scripts": {
    "test": "mocha-webpack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
  }
}

Несколько вещей, о том что мы сделали:

  • Флаг --webpack-config указывает конфигурационный файл webpack для использования в тестах. В большинстве случаев это будут идентичная конфигурация, используемой в проекте, с одной небольшой доработкой. Мы поговорим об этом позднее.

  • Флаг --require гарантирует, что файл test/setup.js будет запущен перед любыми тестами, в котором мы можем настроить для наших тестов глобальное окружение, в котором они будут запускаться.

  • Последний аргумент — это шаблон для тестовых файлов, которые будут включены в тестовую сборку.

Дополнительная конфигурация webpack

Вынесение внешних NPM-зависимостей

В наших тестах мы, скорее всего, импортируем ряд NPM-зависимостей — некоторые из этих модулей могут быть написаны не для использования в браузере и просто не смогут быть корректно добавлены в сборку webpack. Другой плюс в том, что извлечение внешних зависимостей значительно улучшит скорость загрузки тестов. Мы можем вынести все NPM-зависимости с помощью webpack-node-externals:

// webpack.config.js
const nodeExternals = require('webpack-node-externals')

module.exports = {
  // ...
  externals: [nodeExternals()]
}

Source Maps

Source maps должны быть встроены для использования в mocha-webpack. Рекомендуемая конфигурация:

module.exports = {
  // ...
  devtool: 'inline-cheap-module-source-map'
}

При отладке через IDE рекомендуется также добавить следующее:

module.exports = {
  // ...
  output: {
    // ...
    // использовать абсолютные пути в sourcemaps (важно для отладки через IDE)
    devtoolModuleFilenameTemplate: '[absolute-resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
  }
}

Настройка браузерного окружения

vue-test-utils требует браузерного окружения для запуска. Мы можем симулировать его в Node используя jsdom-global:

npm install --save-dev jsdom jsdom-global

Затем в test/setup.js:

require('jsdom-global')()

Это добавит браузерное окружение в Node, таким образом vue-test-utils сможет корректно запуститься.

Выбор библиотеки утверждений

Chai — популярная библиотека утверждений, которая обычно используется вместе с Mocha. Вы также можете воспользоваться Sinon для создания шпионов и заглушек.

В качестве альтернативы вы можете использовать expect, который является частью Jest и реализует точно такой же API в документации Jest.

Мы будем использовать expect здесь и сделаем его глобально доступным, чтобы нам не приходилось импортировать его в каждом тесте:

npm install --save-dev expect

Затем в test/setup.js:

require('jsdom-global')()

global.expect = require('expect')

Оптимизация Babel для тестов

Обратите внимание, что мы используем babel-loader для обработки JavaScript. У вас уже должен быть настроен Babel, если вы используете его в своём приложении, через файл .babelrc. Здесь babel-loader будет автоматически использовать тот же файл конфигурации.

Следует отметить, что если вы используете Node 6+, которая уже поддерживает большинство функций ES2015, вы можете настроить отдельную опцию env Babel, которая будет транспилировать только те функции, которые ещё не поддерживаются в используемой версии Node (например, stage-2 или поддержку синтаксиса flow, и т.п.).

Добавление теста

Создайте файл в src названный Counter.vue:

<template>
	<div>
	  {{ count }}
	  <button @click="increment">Увеличить</button>
	</div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

И создайте файл теста, названный test/Counter.spec.js со следующим кодом:

import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('увеличивает счётчик по нажатию кнопки', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).toMatch('1')
  })
})

И теперь мы можем запустить тест:

npm run test

Ура, мы запустили наши тесты!

Покрытие кода (Coverage)

Для настройки покрытия кода в mocha-webpack, следуйте инструкции по настройке покрытия кода mocha-webpack.

Ресурсы

Тестирование однофайловых компонентов с Karma

Пример проекта для этой конфигурации доступен на GitHub.

Karma — это программа для запуска тестов, которая открывает браузеры, выполняет тесты и сообщает нам об их результатах. Мы собираемся использовать фреймворк Mocha для написания тестов. Мы будем использовать библиотеку chai для тестовых утверждений.

Установка Mocha

Предположим, что вы начинаете с конфигурации, где правильно настроены webpack, vue-loader и Babel — например, развёрнутый шаблон webpack-simple с помощью vue-cli.

Первым делом нам необходимо установить тестовые зависимости:

npm install --save-dev @vue/test-utils karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha

Далее нам нужно определить скрипт для запуска тестов в package.json.

// package.json
{
  "scripts": {
    "test": "karma start --single-run"
  }
}
  • Флаг --single-run указывает Karma запускать набор тестов только один раз.

Karma Configuration

Создайте файл karma.conf.js в корне вашего проекта:

// karma.conf.js

var webpackConfig = require('./webpack.config.js')

module.exports = function (config) {
  config.set({
    frameworks: ['mocha'],

    files: [
      'test/**/*.spec.js'
    ],

    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },

    webpack: webpackConfig,

    reporters: ['spec'],

    browsers: ['Chrome']
  })
}

Этот файл используется для настройки Karma.

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

В нашей конфигурации мы запускаем тесты в Chrome. Для добавления дополнительных браузеров смотрите раздел Браузеры в документации Karma.

Выбор библиотеки утверждений

Chai - популярная библиотека утверждений, которая обычно используется вместе с Mocha. Вы посмотреть на Sinon для создания шпионов и заглушек.

Мы можем установить плагин karma-chai для использования chai в наших тестах.

npm install --save-dev karma-chai

Добавление теста

Создайте в каталоге src файл с именем Counter.vue:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  },

  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

И создайте файл test/Counter.spec.js со следующим кодом:

import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Counter from '../src/Counter.vue'

describe('Counter.vue', () => {
  it('increments count when button is clicked', () => {
    const wrapper = shallowMount(Counter)
    wrapper.find('button').trigger('click')
    expect(wrapper.find('div').text()).contains('1')
  })
})

И теперь мы можем запустить тесты:

npm run test

Ура, наши тесты работают!

Покрытие кода

Для настройки покрытия кода Karma, мы можем использовать плагин karma-coverage.

По умолчанию karma-coverage не будет использовать исходные карты для отображения отчётов о покрытии. Поэтому нам нужно использовать babel-plugin-istanbul, чтобы убедиться, что покрытие правильно работает.

Установите karma-coverage, babel-plugin-istanbul и cross-env:

npm install --save-dev karma-coverage cross-env

Мы собираемся использовать cross-env для установки переменной окружения BABEL_ENV. Таким образом, мы можем использовать babel-plugin-istanbul при компиляции наших тестов — мы не хотим включать babel-plugin-istnabul при компиляции нашего кода в production:

npm install --save-dev babel-plugin-istanbul

Обновите файл .babelrc для использования babel-plugin-istanbul, когда BABEL_ENV равняется test:

{
  "presets": [
    ["env", { "modules": false }],
    "stage-3"
  ],
  "env": {
    "test": {
      "plugins": ["istanbul"]
    }
  }
}

Теперь обновите файл karma.conf.js для использования покрытия кода. Добавьте coverage в массив reporters и добавьте поле coverageReporter:

// karma.conf.js

module.exports = function (config) {
  config.set({
  // ...

    reporters: ['spec', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })
}

И обновите тестовый скрипт test для установки BABEL_ENV:

// package.json
{
  "scripts": {
    "test": "cross-env BABEL_ENV=test karma start --single-run"
  }
}

Ресурсы

Тестирование асинхронной логики

Чтобы упростить тестирование, vue-test-utils применяет обновления DOM синхронно. Однако, есть некоторые тонкости, когда вам необходимо протестировать компонент с асинхронным поведением, таким как коллбэки или промисы.

Одними из самых распространённых поведений являются запросы к API и действия Vuex. В примерах ниже будет показано как протестировать метод, который делает запрос к API. В этом примере используется Jest для запуска тестов и мок для HTTP-библиотеки axios. Подробнее о использовании моков в Jest можно прочитать здесь.

Реализация мока для axios выглядит так:

export default {
  get: () => Promise.resolve({ data: 'value' })
}

Компонент ниже делает вызов к API при нажатии кнопки и сохраняет полученный ответ в value.

<template>
  <button @click="fetchResults" />
</template>

<script>
  import axios from 'axios'

  export default {
    data () {
      return {
        value: null
      }
    },

    methods: {
      async fetchResults () {
        const response = await axios.get('mock/service')
        this.value = response.data
      }
    }
  }
</script>

Тест можно написать следующим образом:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')

it('делает асинхронный запрос при нажатии кнопки', () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  expect(wrapper.vm.value).toBe('value')
})

В настоящее время этот тест не будет успешно проходить, потому что проверка значения вызывается до разрешения промиса fetchResults. Большинство библиотек для модульного тестирования предоставляют коллбэк, чтобы предоставить возможность определять когда тест должен будет завершаться. Jest и Mocha используют done. Мы можем использовать done в комбинации с $nextTick или setTimeout, чтобы гарантировать, что любые промисы будут разрешены перед проверками.

it('делает асинхронный запрос при нажатии кнопки', (done) => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  wrapper.vm.$nextTick(() => {
    expect(wrapper.vm.value).toBe('value')
    done()
  })
})

Необходимость $nextTick или setTimeout требуется для прохождения теста, потому что очередь микрозадач, в которой обрабатываются промисы, обрабатывается до очереди задач, где обрабатываются $nextTick и setTimeout. Это означает, что к моменту запуска $nexTick и setTimeout, будут выполнены любые коллбэки промисов. См. здесь для более подробного объяснения.

Другое решение — использовать async функцию и npm-пакет flush-promises. flush-promises сбрасывает все ожидаемые промисы. Вы можете использовать await вызов для flushPromises чтобы очистить все ожидаемые промисы и улучшить читаемость вашего теста.

Обновлённый тест будет выглядеть так:

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

it('делает асинхронный запрос при нажатии кнопки', async () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  await flushPromises()
  expect(wrapper.vm.value).toBe('value')
})

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

Using with TypeScript

An example project for this setup is available on GitHub.

TypeScript is a popular superset of JavaScript that adds types and classes on top of regular JS. Vue Test Utils includes types in the distributed package, so it works well with TypeScript.

In this guide, we'll walk through how to setup a testing setup for a TypeScript project using Jest and Vue Test Utils from a basic Vue CLI TypeScript setup.

Adding TypeScript

First you need to create a project. If you don't have Vue CLI installed, install it globally:

$ npm install -g @vue/cli

And create a project by running:

$ vue create hello-world

In the CLI prompt, choose to Manually select features, select TypeScript, and press enter. This will create a project with TypeScript already configured.

NOTE

If you want a more detailed guide on setting up Vue with TypeScript, checkout the TypeScript Vue starter guide.

The next step is to add Jest to the project.

Setting up Jest

Jest is a test runner developed by Facebook, aiming to deliver a battery-included unit testing solution. You can learn more about Jest on its official documentation.

Install Jest and Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

Next define a test:unit script in package.json.

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

Processing Single-File Components in Jest

To teach Jest how to process *.vue files, we need to install and configure the vue-jest preprocessor:

npm install --save-dev vue-jest

Next, create a jest block in package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest",
    },
    "testURL": "http://localhost/"
  }
}

Configuring TypeScript for Jest

In order to use TypeScript files in tests, we need to set up Jest to compile TypeScript. For that we need to install ts-jest:

$ npm install --save-dev ts-jest

Next, we need to tell Jest to process TypeScript test files with ts-jest by adding an entry under jest.transform in package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process `*.ts` files with `ts-jest`
      "^.+\\.tsx?$": "ts-jest"
    },
    // ...
  }
}

Placing Test Files

By default, Jest will recursively pick up all files that have a .spec.js or .test.js extension in the entire project.

To run test files with a .ts extension, we need to change the testRegex in the config section in the package.json file.

Add the following to the jest field in package.json:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest recommends creating a __tests__ directory right next to the code being tested, but feel free to structure your tests as you see fit. Just beware that Jest would create a __snapshots__ directory next to test files that performs snapshot testing.

Writing a unit test

Now we've got the project set up, it's time to write a unit test.

Create a src/components/__tests__/HelloWorld.spec.ts file, and add the following code:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

That's all we need to do to get TypeScript and Vue Test Utils working together!

Resources

Использование с Vue Router

Установка Vue Router в тестах

Вы никогда не должны устанавливать Vue Router в базовый конструктор Vue в тестах. Установка Vue Router добавляет $route и $router как свойства только для чтения на прототипе Vue.

Чтобы этого избежать, мы можем создать localVue и установить Vue Router на него.

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Примечание: Установка Vue Router на localVue также добавляет $route и $router в качестве свойство только для чтения localVue. Это означает, что вы можете использовать опцию mocks для перезаписи $route и $router при монтировании компонента, используя localVue с установленным Vue Router.

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

Когда мы запускаем тесты, нам нужно сделать эти компоненты vue-router доступными для компонента, который мы монтируем. Есть два способа сделать это.

Использование заглушек (stubs)

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

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

Установка Vue Router с помощью localVue

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)

shallowMount(Component, {
  localVue
})

Создание моков $route и $router

Иногда вам может потребоваться протестировать, что компонент что-то делает с параметрами объектов $route и $router. Для этого вы можете передавать пользовательские моки в экземпляр Vue.

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

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

Известные подводные камни

Установка Vue Router добавляет $route и $router в качестве свойств только для чтения на прототипе Vue.

Это означает, что любые будущие тесты, которые попытаются сделать мок $route или $router потерпят неудачу.

Для избежания этого никогда не устанавливайте Vue Router глобально при запуске тестов; используйте localVue как было показано выше.

Использование с Vuex

В этом руководстве мы рассмотрим как тестировать Vuex в компонентах с Vue Test Utils и как подходить к тестированию хранилища Vuex.

Тестирование Vuex в компонентах

Создание моков для действий

Давайте посмотрим на часть кода.

Это компонент который мы хотим протестировать. Он вызывает действие Vuex.

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Нажми</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'actionClick'
    ]),
    actionInputIfTrue: function actionInputIfTrue (event) {
      const inputValue = event.target.value
      if (inputValue === 'input') {
        this.$store.dispatch('actionInput', { inputValue })
      }
    }
  }
}
</script>

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

Чтобы протестировать это, нам нужно передать мок хранилища в Vue, когда мы отделяем наш компонент.

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

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

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      state: {},
      actions
    })
  })

  it('вызывает "actionInput", когда значение события — "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('не вызывает "actionInput", когда значение событие не "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('вызывает действие хранилища "actionClick" по нажатию кнопки', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

Что тут происходит? Сначала мы указываем Vue использовать Vuex с помощью метода localVue.use. Это всего лишь обёртка вокруг Vue.use.

Затем мы создаём мок хранилища вызовом new Vuex.store с нашими заготовленными значениями. Мы передаём ему только действия, так как это всё, что нам необходимо.

Действия реализуются с помощью mock-функций jest. Эти mock-функции предоставляют нам методы для проверки, вызывались ли действия или нет.

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

Теперь способ, которым мы определяем наше хранилище выглядит немного необычным для вас.

Мы используем beforeEach, чтобы убедиться, что у нас есть чистое хранилище перед каждым тестом. beforeEach — это хук в mocha, который вызывается перед каждым тестом. В нашем тесте мы переназначаем значения переменных хранилища. Если бы мы этого не сделали, mock-функции нужно было бы автоматически сбрасывать. Это также позволяет нам изменять состояние в наших тестах, не влияя на последующие тесты.

Самое важно, что следует отметить в этом тесте — то что мы создаём мок хранилища Vuex и затем передаём его в vue-test-utils.

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

Создание моков для геттеров

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default{
  computed: mapGetters([
    'clicks',
    'inputValue'
  ])
}
</script>

Это довольно простой компонент. Он отображает результат геттеров clicks и inputValue. Опять же, нас не волнует что возвращают эти геттеры — только то, что их результат будет корректно отображён.

Давайте посмотрим на тест:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Отображает "state.inputValue" в первом теге p', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Отображает "state.clicks" во втором теге p', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

Этот тест очень похож на тест действий. Мы создаём мок хранилища перед каждым тестом, передаём его в качестве опции когда вызываем shallowMount, и проверяем что значение вернувшееся из мока-геттера отображается.

Это здорово, но что, если мы хотим проверить, что наши геттеры возвращают корректную часть нашего состояния?

Создание моков с модулями

Модули полезны для разделения нашего хранилища на управляемые части. Они также экспортируют геттеры. Мы можем использовать их в наших тестах.

Давайте взглянем на наш компонент:

<template>
  <div>
    <button @click="moduleActionClick()">Нажми</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default{
  methods: {
    ...mapActions([
      'moduleActionClick'
    ])
  },

  computed: mapGetters([
    'moduleClicks'
  ])
}
</script>

Простой компонент, который содержит одно действие и один геттер.

И тест:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('MyComponent.vue', () => {
  let actions
  let state
  let store

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters
        }
      }
    })
  })

  it('вызывает действие "moduleActionClick" при нажатии кнопки', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const button = wrapper.find('button')
    button.trigger('click')
    expect(actions.moduleActionClick).toHaveBeenCalled()
  })

  it('отображает "state.inputValue" в первом теге p', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

Тестирование хранилища Vuex

Существуют два подхода к тестированию хранилища Vuex. Первый подход заключается в модульном тестировании геттеров, изменений и действий отдельно. Второй подход — создать хранилище и протестировать его. Мы рассмотрим оба подхода.

Чтобы понять, как протестировать хранилище Vuex, мы создадим простое хранилище-счётчик. В хранилище есть мутация increment и геттер evenOrOdd.

// mutations.js
export default {
  increment (state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

Тестирование геттеров, мутаций и действий отдельно

Геттеры, мутации и действия — JavaScript-функции, поэтому мы можем протестировать их без использования Vue Test Utils и Vuex.

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

Мы создадим два тестовых файла: mutations.spec.js и getters.spec.js:

Сначала давайте протестируем мутации increment:

// mutations.spec.js

import mutations from './mutations'

test('increment increments state.count by 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

Теперь давайте протестируем геттер evenOrOdd. Мы можем протестировать его, путём создания мока для state, вызвав геттер с state и проверкой, что возвращается корректное значение.

// getters.spec.js

import getters from './getters'

test('evenOrOdd возвращает even, если в state.count находится even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('evenOrOdd возвращает odd, если в state.count находится odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

Тестирование запущенного хранилища

Другой подход к тестированию хранилища Vuex — это создание запущенного хранилища с использованием конфигурации хранилища.

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

Недостатком является то, что если тест ломается, может быть трудно найти, в чём проблема.

Давайте напишем тест. Когда мы создаём, мы будем использовать localVue, чтобы избежать загрязнения базового конструктора Vue. Тест создаёт хранилище, используя экспорт store-config.js:

// store-config.spec.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('инкрементирует значение счётчика, когда происходит инкремент', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('обновляет геттер evenOrOdd, когда происходит инкремент', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

Обратите внимание, что мы используем cloneDeep для клонирования конфигурации хранилища перед созданием хранилища с ним. Это связано с тем, что Vuex мутирует объект с опциями, используемый для создания хранилища. Чтобы убедиться, у нас есть пустое хранилище в каждом тесте, нам нужно клонировать объект storeConfig.

Ресурсы