Vue-Test-Utils + Jest Unit Test Введение и практика

модульный тест

представлять

Vue-Test-UtilsдаVue.jsОфициальная библиотека утилит модульного тестирования, которая предоставляет рядAPIчтобы нам было удобно писатьVueМодульные тесты в приложении.

Существует много основных средств запуска модульных тестов, таких какJest,Mochaа такжеKarmaподождите, этоVue-Test-UtilsВ документации есть соответствующие туториалы, здесь мы только знакомимVue-Test-Utils + JestКомбинированный пример.

Jest — это среда тестирования, разработанная Facebook. Vue описывает это так: это самый полнофункциональный тест-раннер. Он требует минимальной настройки, JSDOM установлен по умолчанию, утверждения встроены, а пользовательский интерфейс командной строки великолепен. Но вам нужен препроцессор, который может импортировать однофайловые компоненты в ваши тесты. Мы создали препроцессор vue-jest для обработки наиболее распространенных функций компонентов с одним файлом, но он по-прежнему не на 100% справляется с тем, что делает vue-loader.

Конфигурация среды

через строительные лесаvue-cliКогда вы создаете новый проект, если вы выбираетеUnit Testingмодульный тест и выберитеJestПри выполнении тестов после создания проекта среда, необходимая для модульного тестирования, будет автоматически настроена и может использоваться напрямую.Vue-Test-Utilsа такжеJestизAPIПришло время писать тест-кейсы.

Однако в начале нового проекта функция юнит-тестирования не была выбрана, если нужно добавить ее позже, есть два варианта:

Первая конфигурация:

Добавить прямо в проектunit-jestПлагин автоматически установит и настроит необходимые зависимости.

vue add @vue/unit-jest

Вторая конфигурация:

Эта конфигурация будет немного хлопотной, ниже приведены конкретные шаги.

Установить зависимости

  • УстановитьJestа такжеVue Test Utils

    npm install --save-dev jest @vue/test-utils
    
  • Установитьbabel-jest,vue-jestа также7.0.0-bridge.0версияbabel-core

    npm install --save-dev babel-jest vue-jest babel-core@7.0.0-bridge.0
    
  • Установитьjest-serializer-vue

    npm install --save-dev jest-serializer-vue
    

настроитьJest

JestКонфигурацию можно найти вpackage.jsonконфигурации; вы также можете создать новый файлjest.config.js, поместите его в корневой каталог проекта. Здесь я выбираю настройку вjest.config.jsсередина:

module.exports = {
    moduleFileExtensions: [
        'js',
        'vue'
    ],
    transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'
    ],
    testMatch: ['**/__tests__/**/*.spec.js'],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
}

Описание каждого элемента конфигурации:

  • moduleFileExtensionsРассказыватьJestсуффикс файла, который должен совпадать
  • transformсоответствовать.vueфайл при использованииvue-jestобработка, соответствие.jsфайл при использованииbabel-jestиметь дело с
  • moduleNameMapperиметь дело сwebpackпсевдонимы, такие как:@выражать/srcсодержание
  • snapshotSerializersСериализуйте сохраненные результаты тестов моментальных снимков, чтобы сделать их более красивыми
  • testMatchКакие файлы сопоставить для тестирования
  • transformIgnorePatternsКаталог не соответствует

настроитьpackage.json

Напишите командный скрипт, выполняющий тест:

{
    "script": {
        "test": "jest"
    }
}

первый тест

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

использоватьvue-cliСоздать проект

В настоящее время я использую3.10.0версияvue-cli. Начните создавать проект:

vue create first-vue-jest

выберитеManually select featuresЧтобы настроить функцию ручного выбора:

Vue CLI v3.10.0
┌───────────────────────────┐
│  Update available: 4.0.4  │
└───────────────────────────┘
? Please pick a preset:
  VUE-CLI3 (vue-router, node-sass, babel, eslint)
  default (babel, eslint)
❯ Manually select features

чек об оплатеBabel,Unit Testing:

? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◉ Unit Testing
 ◯ E2E Testing

выберитеJest:

? Pick a unit testing solution:
  Mocha + Chai
❯ Jest

выберитеIn dedicated config filesНастройте каждую информацию о конфигурации в соответствующемconfigВ файле:

? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
  In package.json

Введите n без сохранения пресета:

? Save this as a preset for future projects? (y/N) n

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

babel.config.js:

module.exports = {
    presets: [
        '@vue/cli-plugin-babel/preset'
    ]
}

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

module.exports = {
    preset: '@vue/cli-plugin-unit-jest'
}

package.json:

{
    "name": "first-vue-jest",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "test:unit": "vue-cli-service test:unit"
    },
    "dependencies": {
        "core-js": "^3.1.2",
        "vue": "^2.6.10"
    },
    "devDependencies": {
        "@vue/cli-plugin-babel": "^4.0.0",
        "@vue/cli-plugin-unit-jest": "^4.0.0",
        "@vue/cli-service": "^4.0.0",
        "@vue/test-utils": "1.0.0-beta.29",
        "vue-template-compiler": "^2.6.10"
    }
}

выполнить тестовую команду

После завершения проекта с проектом, созданным на шагах выше, мы можемpackage.jsonизscriptsя вижуtest:unit, выполните его:

cd first-vue-jest
npm run test:unit

Затем вы увидите вывод в терминале,PASSУказывает, что тестовый пример пройден, это официальный пример модульного теста. Теперь напишем что-нибудь свое.

first-vue-jest-result

Реализовать ToDoList

to-do-list

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

  • Введите то, что нужно сделать, в поле ввода с правой стороны головы, нажмите Enter, содержимое перейдет в список для завершения, и поле ввода одновременно будет очищено.
  • Когда поле ввода пусто, нажмите Enter, не внося никаких изменений.
  • Список ожидания поддерживает редактирование, но завершенный список редактировать нельзя.
  • Справа от каждого элемента списка есть кнопка удаления,-означает, нажмите, чтобы удалить элемент
  • В списке дел есть кнопка, помеченная как выполненная, сЗнак указывает на то, что текущий элемент перемещается в завершенный список после нажатия
  • Завершенные списки имеют кнопки, помеченные как незавершенные сxСимвол указывает на то, что текущий элемент перемещается в незавершенный список после нажатия
  • Номер списка увеличивается с 1
  • Когда список дел пуст, слово to-do не отображается
  • Когда заполненный список пуст, слово Completed не отображается.

Сначала напишите указанную выше страницу

Прежде чем писать страницу, сгенерируйте ее при создании проектаHelloWorld.vueи соответствующий тестовый файлexample.spec.jsудалять; изменятьApp.vueфайл, импортToDoListКомпоненты:

<template>
    <div id="app">
        <ToDoList></ToDoList>
    </div>
</template>

<script>
import ToDoList from './components/ToDoList'

export default {
    components: {
        ToDoList
    }
}
</script>

существуетsrc/compoentsсоздать новый файлToDoList.vue, я не буду публиковать его, если есть еще стили, вы можете увидеть его для деталей.Исходный код этого проекта:

<template>
    <div class="todolist">
        <header>
            <h5>ToDoList</h5>
            <input class="to-do-text" 
                v-model="toDoText" 
                @keyup.enter="enterText" 
                placeholder="输入计划要做的事情"/>
        </header>
        <h4 v-show="toDoList.length > 0">待完成</h4>
        <ul class="wait-to-do">
            <li v-for="(item, index) in toDoList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" @blur="setValue(index, $event)" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToComplete(item, index)">√</span>
                    <span class="del" @click="deleteWait(index)">-</span>
                </p>
            </li>
        </ul>
        <h4 v-show="completedList.length > 0">已完成</h4>
        <ul class="has-completed">
            <li v-for="(item, index) in completedList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" disabled="true" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToWait(item, index)">x</span>
                    <span class="del" @click="deleteComplete(index)">-</span>
                </p>
            </li>
        </ul>
    </div>
</template>
<script>
export default {
    data() {
        return {
            toDoText: '',
            toDoList: [],
            completedList: []
        }
    },
    methods: {
        setValue(index, e) {
            this.toDoList.splice(index, 1, e.target.value)
        },
        removeToComplete(item, index) {
            this.completedList.splice(this.completedList.length, 0, item)
            this.toDoList.splice(index, 1)
        },
        removeToWait(item, index) {
            this.toDoList.splice(this.toDoList.length, 0, item)
            this.completedList.splice(index, 1)
        },
        enterText() {
            if (this.toDoText.trim().length > 0) {
                this.toDoList.splice(this.toDoList.length, 0, this.toDoText)
                this.toDoText = ''
            }
        },
        deleteWait(index) {
            this.toDoList.splice(index, 1)
        },
        deleteComplete(index) {
            this.completedList.splice(index, 1)
        }
    }
};
</script>

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

Изменить конфигурацию каталога

Следующий шаг — начать писать файлы модульных тестов.Перед записью мы сначала изменим каталог тестовых файлов как__tests__, при измененииjest.config.jsДля следующей конфигурации обратите внимание наtestMatchбыл изменен, чтобы соответствовать__tests__все в каталоге.jsфайл.

module.exports = {
    moduleFileExtensions: [
        'js',
        'vue'
    ],
    transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'
    ],
    testMatch: ['**/__tests__/**/*.spec.js'],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
}

написать тестовые файлы

существует__tests__/unit/Создайте новый файл в каталогеtodolist.spec.js, мы договорились протестироватьvueфайл, то его файл модульного теста обычно называется*.spec.jsили*.test.js.

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

describe('test ToDoList', () => {
    it('输入框初始值为空字符串', () => {
        const wrapper = shallowMount(ToDoList)
        expect(wrapper.vm.toDoText).toBe('')
    })
})

Приведенный выше тестовый файл кратко описывает:

  • shallowMountсоздаст файл, содержащий смонтированный и визуализированныйVueкомпонентWrapper, который заглушает только текущий компонент, а не дочерние компоненты.
  • describe(name, fn)Вот определение набора тестов,test ToDoListимя набора тестов,fnэто конкретная исполняемая функция
  • it(name, fn)является тестовым случаем,输入框初始值为空字符串имя тестового случая,fnЭто конкретная исполняемая функция; в наборе тестов можно защитить несколько тестовых случаев.
  • expectдаJestВстроенный стиль утверждения, в отрасли существуют и другие стили утверждения, такие какShould,AssertЖдать.
  • toBeдаJestПредоставленные методы утверждения, больше можно найти вJest ExpectСм. конкретное использование.
it('待完成列表初始值应该为空数组', () => {
    const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.toDoList.length).toBe(0)
})

it('已完成列表初始值应该为空数组', () => {
    const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.completedList).toEqual([])
})

Списки подлежащих завершению и завершенных списков на самом деле являются списками, поэтому поле, в котором хранятся данные, должно бытьArrayтип, пустой список — это пустой массив. Если второй тестовый пример изменить на:

expect(wrapper.vm.completedList).toBe([])

выдаст ошибку, потому чтоtoBeВнутри метода находится вызовObject.is(value1, value2)сравнить 2 значения на равенство, и==или===Логика суждений другая. очевидноObject.is([], [])вернусьfalse.

it('输入框值变化的时候,toDoText应该跟着变化', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.find('.to-do-text').setValue('晚上要陪妈妈逛超市')
    expect(wrapper.vm.toDoText).toBe('晚上要陪妈妈逛超市')
})
it('输入框没有值,敲入回车的时候,无变化', () => {
    const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue('')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length)
})
it('输入框有值,敲入回车的时候,待完成列表将新增一条数据,同时清空输入框', () => {
    const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue('晚上去吃大餐')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length + 1)
    expect(wrapper.vm.toDoText).toBe('')
})
  • setValueВы можете установить значение текстового элемента управления и обновить его.v-modelсвязанные данные.
  • .to-do-textЯвляетсяCSSСелектор;Vue-Test-Utilsпри условииfindметод для возвратаWrapper; селектор может бытьCSSселектор, может бытьVueКомпонент также может быть объектом, содержащимnameилиrefсвойства, например, можно использовать так:wrapper.find({ name: 'my-button' })
  • wrapper.vmЯвляетсяVueпример, толькоVueОбертка компонента имеет толькоvmэто имущество; поwrapper.vmиметь доступ ко всемVueСвойства и методы экземпляра. Например:wrapper.vm.$data,wrapper.vm.$nextTick().
  • triggerметод может быть использован для запускаDOMСобытия, инициированные здесь события, все синхронны, поэтому нет необходимости помещать утверждение в$nextTick()В то же время он поддерживает передачу объекта, и при захвате события можно получить свойства входящего объекта. Это можно написать так:wrapper.trigger('click', {name: "bubuzou.com"})
it('待完成列表支持编辑功能,编辑后更新toDoList数组', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['跑步半小时']})
    wrapper.find('.wait-to-do li').find('input').setValue('绕着公园跑3圈') 
    wrapper.find('.wait-to-do li').find('input').trigger('blur') 
    expect(wrapper.vm.toDoList[0]).toBe('绕着公园跑3圈')
})

Сначала сsetDataДатьtoDoListУстановите начальное значение, чтобы оно отображало элемент списка; затем найдите элемент списка и используйтеsetValueЗадаем ему значение, имитируя редактирование, используется поле ввода элемента списка:value="item"границаvalue, такsetValueОбновления не могут быть запущены; только черезtriggerзапустить обновлениеtoDoListценность .

it('待完成列表点击删除,同时更新toDoList数组', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['睡前看一小时书']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    wrapper.find('.wait-to-do li').find('.del').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
})
it('点击待完成列表中某项的已完成按钮,数据对应更新', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['中午饭后吃一个苹果']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
    wrapper.find('.wait-to-do li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
})
it('点击已完成列表中某项的未完成按钮,数据对应更新', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({completedList: ['唱了一首歌']})
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
    wrapper.find('.has-completed li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
})
it('列表序号从1开始递增', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['早上做作业', '下午去逛街']})
    expect(wrapper.vm.toDoList.length).toBe(2)
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>1</i>')
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>2</i>')
})
it('当待完成列表为空的时候,不显示待完成字样', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: []})
    expect(wrapper.find('h4').isVisible()).toBeFalsy()
    wrapper.setData({toDoList: ['明天去爬北山']})
    expect(wrapper.find('h4').isVisible()).toBeTruthy()
})

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

Асинхронное тестирование

Наконец, чтобы имитировать асинхронное тестирование, мы добавляем требование, то есть при загрузке страницы она будет запрашивать данные удаленного списка ожидания. Создайте новый проект в корневом каталоге проекта__mocks__каталог и создайте новыйaxios.js:

const toToList = {
    success: true,
    data: ['上午去图书馆看书', '下去出去逛街']
}

export const get = (url) => {
    if (url === 'toToList.json') {
        return new Promise((resolve, reject) => {
            if (toToList.success) {
                resolve(toToList)
            } else {
                reject(new Error())
            }
        })
    }
}

ИсправлятьToDoList.vue, импортaxiosи увеличитьmounted:

<script>
import * as axios from '../../__mocks__/axios'
export default {
    mounted () {
        axios.get('toToList.json').then(res => {
            this.toDoList = res.data
        }).catch(err => {
            
        })
    },
};
</script>

Тестовый случай записывается так:

it('当页面挂载的时候去请求数据,请求成功后应该会返回2条数据', (done) => {
    wrapper.vm.$nextTick(() => {
        expect(wrapper.vm.toDoList.length).toBe(2)
        done()
    })
})

Для асинхронного кода при написании утверждений его нужно поместить вwrapper.vm.$nextTick(), и вызовите его вручнуюdone().

Настроить тестовое покрытие

Тест-кейс написан частично, если смотреть на покрытие, то нужно настроить тестовое покрытие. существуетjest.config.jsНовая конфигурация в:

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

существуетpackage.jsonизscriptsДобавлена ​​новая конфигурация:

"test:cov": "vue-cli-service test:unit --coverage"

Затем запускаем в терминале:npm run test:cov, результат следующий:

test:cov1

После запуска именования тестового покрытия оно будет сгенерировано в корневом каталоге проекта.coverageкаталог, браузер открываетindex.html:

test:cov2