В течение долгого времени юнит-тестирование не было навыком, которым должны обладать фронтенд-инженеры.Однако с развитием фронтенд-инжиниринга проекты становятся все более сложными, а коды стремятся к повторному использованию.Один из ключевых факторов качества проекта
1. В чем смысл модульного тестирования?
- При крупномасштабном рефакторинге кода можно гарантировать корректность рефакторинга.
- Обеспечение качества кода и проверка функциональной целостности
2. Понимание основных сред тестирования интерфейса.
2.1 Сравнение рамки (основной три)
- Karma — инструмент управления процессом выполнения тестов JavaScript на основе Node.js (Test Runner), позволяющий автоматически запускать ваш код в нескольких браузерах (chrome, firefox и т. д.)
- Mocha — Mocha — это среда тестирования, реализующая модульное тестирование с помощью библиотеки утверждений chai в vue-cli (Mocha+chai).
- jest — Jest — это среда тестирования JavaScript, разработанная Facebook. Широко используется внутри Facebook для тестирования различного кода JavaScript.
2.2 Классификация модульных тестов
- TDD - (Test Driven Development) фокусируется на разработке и использует тестовые примеры для стандартизации и ограничения разработчиков для написания кода более высокого качества и с меньшим количеством ошибок.
- BDD — (Behavior Driven Development) Подход к разработке, при котором бизнес-результаты определяются извне, а затем детализируются до момента, когда эти результаты могут быть достигнуты, при этом каждый результат преобразуется в соответствующие критерии приемлемости включения.
Проще говоря, TDD сначала пишет тестовый модуль, затем код основной функции, а затем позволяет тестовому модулю пройти тест, в то время как BDD сначала пишет основной функциональный модуль, а затем пишет тестовый модуль.
2.3 Библиотека утверждений
Утверждения относятся к некоторым логическим выражениям. Значение выражения истинно в определенной точке программы, и оценивается, соответствует ли фактический результат выполнения кода ожидаемому результату. Библиотека утверждений инкапсулирует часто используемые методы.
Основные библиотеки утверждений:
- assert (TDD)
assert("mike" == user.name);
- expect.js (BDD) — утверждения в стиле expect()
expect(foo).to.be("aa");
- should.js — стиль BDD (Behavior Driven Development) во всем
foo.should.be("aa"); //should
- chai (BDD/TDD) — интегрирует expect(), assert() и должен стилизовать утверждения
3. Приложение Jest для модульного тестирования
Jest — это среда модульного тестирования JS с открытым исходным кодом от Facebook.Это также среда модульного тестирования, которая в настоящее время используется React.В настоящее время Vue официально также считает ее официальной рекомендацией среды модульного тестирования. Помимо Facebook, Twitter, Airbnb также используют Jest. В дополнение к базовым функциям утверждения и имитации Jest также имеет полезные функции, такие как тестирование моментальных снимков, режим мониторинга в реальном времени и отчеты о покрытии. При этом Jest можно использовать практически без настройки.
Я использую jest в качестве фреймворка модульного тестирования при разработке проектов в сочетании с официальным инструментом тестирования vue vue-util-test.
3.1 Шуточная установка
npm install --save-dev jest
npm install -g jest
3.2 Конфигурационный файл Jest
(1) Добавить метод
- Автоматически генерировать Jest.config.js
npx jest --init
Затем будет несколько вариантов, выберите в соответствии с вашей реальной ситуацией
После нажатия Enter он будет автоматически сгенерирован в каталоге проекта.Jest.config.jsКонфигурационный файл, конечно, можно и второй выбрать, создать вручную- Вручную создайте и настройте Jest.config.js
const path = require('path');
module.exports = {
verbose: true,
rootDir: path.resolve(__dirname, '../../../'),
moduleFileExtensions: [
'js',
'json',
'vue',
],
testMatch: [
'<rootDir>/src/test/unit/specs/*.spec.js',
],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transformIgnorePatterns: ['/node_modules/'],
collectCoverage: false,
coverageReporters: ['json', 'html'],
coverageDirectory: '<rootDir>/src/test/unit/coverage',
collectCoverageFrom: [
'src/components/**/*.(js|vue)',
'!src/main.js',
'!src/router/index.js',
'!**/node_modules/**',
],
};
Разбор конфигурации:
- testMatch - файл для сопоставления тестовых случаев
- трансформировать - с
vue-jest
иметь дело с*.vue
файл, сbabel-jest
иметь дело с*.js
документ - moduleNameMapper — поддерживает то же самое в исходном коде
@
->src
псевдоним - coverageDirectory - директория отчета о покрытии, место хранения отчета о тестировании
- CollectcoverageFrom - Test Report хочет охватить эти файлы, каталоги, переднее добавить! Избегает этих файлов
(2) инструмент командной строки jest
{
"name": "test",
"version": "1.0.0",
"scripts": {
"unit": "jest --config src/test/unit/jest.conf.js --coverage",
},
dependencies": {
"vue-jest": "^3.0.5",
},
"devDependencies":{
"@vue/test-utils": "^1.0.0-beta.13",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^21.2.0",
"jest": "^21.2.1",
}
}
-
config — настроить путь к файлу конфигурации jest
-
покрытие — создание отчетов о тестовом покрытии
покрытие — это команда, предоставляемая jest для создания отчетов о покрытии тестов. Чтобы создать отчеты о покрытии, добавьте параметр --coverage в package.json
(3) Именование файла модульного теста
Названный в конце spec.js, spec является аббревиатурой от sepcification. С точки зрения тестирования, Спецификация относится к техническим деталям данной функции или приложения, которые должны быть выполнены.
(4) Индикатор покрытия отчета модульного тестирования
执行: npm run unit
Выполнение этой команды после настройки напрямую сгенерирует файл покрытия и отобразит обзор покрытия каждого индикатора в терминале.
Откройте index.html в каталоге покрытия на веб-странице, чтобы увидеть отчет о тестировании для каждого компонента.
- Покрытие операторов Выполняется ли каждый оператор?
- Вызывается ли покрытие ветвления каждой функцией?
- Покрытие функций Выполняется ли каждый блок if?
- Линейное покрытие выполняется для каждой строки?
Когда мы завершаем покрытие модульного теста менее 100%, не паникуйте, не нужно стремиться к 100% покрытию, включите основной функциональный модуль. Конечно, если вы хотите установить минимальное обнаружение покрытия, вы можете в конфигурации Дополнение следующее: если покрытие ниже установленного вами порога (80%), результат теста не пройден.
//jest.config.js
coverageThreshold: {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
},
3.3 Общие утверждения Jest
expect(1+1).toBe(2)//判断两个值是否相等,toBe不能判断对象,需要判断对象要使用toEqual
expect({a: 1}).toEqual({a: 1});//会递归检查对象的每个字段
expect(1).not.toBe(2)//判断不等
expect(n).toBeNull(); //判断是否为null
expect(n).toBeTruthy(); //判断结果为true
expect(n).toBeFalsy(); //判断结果为false
expect(value).toBeCloseTo(0.3); // 浮点数判断相等
expect(compileAndroidCode).toThrow(ConfigError); //判断抛出异常
3.4 Экземпляр тестового компонента Jest + Vue Test Utils
Vue Test Utils — это официальная служебная библиотека модульного тестирования Vue.js, которая тестирует компоненты проверочного кода, комбинируя их, охватывая каждый функциональный тест.
//kAuthCode
<template>
<p class="kauthcode">
<span class="kauthcode_btn" v-if="vertification" @click="handleCode"> 获取验证码</span>
<span v-else>{{timer}} 秒重新获取</span>
</p>
</template>
<script>
export default {
name: 'KAuthCode',
props: {
phone: {
type: String,
require: true,
},
type: {
type: String,
default: '1',
require: true,
validator(t) {
return ['1', '2'].includes(t);// 1手机 2邮箱
},
},
validateType: {
type: String,
default: '1',
validator(t) {
return ['1', '2', '3'].includes(t);// 1 消息 2 表单 3自定义
},
},
},
data() {
return {
timer: 60,
vertification: true,
};
},
methods: {
handleCode() {
if (!this.phone) {
switch (this.type) {
case '1':
this.$Message.warning('手机号码不能为空');
break;
case '2':
this.$refs.formRef.validateField('code');
break;
default: break;
}
return;
}
this.getCode();
},
getCode() {
let response;
switch (this.type) {
case '1':
response = this.$api.login.getPhoneCode({ mobileNumber: this.phone });
break;
case '2':
response = this.$api.login.getEmailCode({ email: this.phone });
break;
default: break;
}
response.then(() => {
this.$Message.success('验证码发送成功');
this.vertification = false;
const codeTimer = setInterval(() => {
this.timer -= 1;
if (this.timer <= 0) {
this.vertification = true;
this.timer = 60;
clearInterval(codeTimer);
}
}, 1000);
});
},
},
};
</script>
<style lang="less" scoped>
.kauthcode {
span {
display: inline-block;
width: 100%;
}
}
</style>
тестовый файл
// kAuthCode.spec.js
import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';
import login from '@/service/modules/login.js';
import iviewUI from 'view-design';
const localVue = createLocalVue();
localVue.use(iviewUI);
const testPhone = '18898538706';
jest.mock('@/service/modules/login.js', () => ({
getPhoneCode: () => Promise.resolve({
data: {
answer: 'mock_yes',
image: 'mock.png',
}
})
}))
describe('KAuthCode.vue', () => {
const option = {
propsData: {
// phone: testPhone,
type: '2'
},
mocks: {
$api: {
login
},
},
};
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
const wrapper = mount(KAuthCode, option);
it('设置手机号码', () => {
const getCode = jest.fn();
option.methods = {getCode};
wrapper.find('.kauthcode_btn').trigger('click');
expect(wrapper.vm.phone).toBe(testPhone);
});
it('没有设置手机号码应报错', () => {
wrapper.setData({type:'2'});
const status = wrapper.find('.kauthcode_btn').trigger('click');
expect(status).toBeFalsy();
});
});
3.5 Vue-Test-Utils API
3.5.1 Wrapper
Wrapper — это оболочка, которая включает в себя смонтированный компонент или vnode и методы тестирования этого компонента или vnode. При монтировании компонента с помощью mount(component, option) получается обертка-обертка, которую можно получить с помощью
-
wrapper.vm
Доступ к фактическому экземпляру Vue -
wrapper.setData
Изменить экземпляр -
wrapper.find
Найдите соответствующий дом и запустите событие `wrapper.find('.kauthcode_btn').trigger('click'); -
propsData
- Настройки пропсов при монтировании компонента
import {createLocalVue, mount, shallowMount} from '@vue/test-utils';
import KAuthCode from '@/components/common/KAuthCode.vue';
const option = {
propsData: {
// phone: testPhone,
type: '2'
},
mocks: {
$api: {
login
},
},
};
const wrapper = mount(KAuthCode, option);
ps: Вы также можете монтировать компоненты с помощью мелкого монтирования. Разница в том, что мелкое монтирование не отображает подкомпоненты. Для получения подробных различий вы можете смонтировать один и тот же компонент с помощью методов мелкого монтирования и монтирования соответственно и выполнить тест моментального снимка для просмотра содержимого сгенерированного файла. .
3.5.2 CreateLocalVue
Возвращает класс Vue, чтобы вы могли добавлять компоненты, миксины и устанавливать плагины, не загрязняя глобальный класс Vue.
import {createLocalVue, mount} from '@vue/test-utils';
import iviewUI from 'view-design';
const localVue = createLocalVue();
localVue.use(iviewUI);
3.5.3 Тестовые крючки
beforeEach и afterEach — в одном и том же описании, beforeAll и afterAll будут выполняться в нескольких областях видимости, подходящих для одноразовых настроек.
- beforeEach(fn) что нужно сделать перед каждым тестом, например, восстановить определенные данные в исходное состояние перед тестом
- afterEach(fn) запускается после выполнения каждого теста
- что нужно сделать beforeAll(fn) перед всеми тестами
- afterAll(fn) запускается после выполнения тестового примера
Порядок вызова: beforeAll => beforeEach => afterAll => afterEach
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
3.5.4 Мок-функции
Три API-интерфейса, связанные с функциями Mock: jest.fn(), jest.spyOn(), jest.mock().
- jest.fn() - это самый простой способ создать фиктивную функцию. Если внутренняя реализация функции не определена, jest.fn() вернет undefined в качестве возвращаемого значения. Конечно, вы также можете установить возвращаемое значение , определите внутреннюю реализацию или верните объект Promise, как в следующем примере:
// 断言mockFn执行后返回值为name
it('jest.fn()返回值', () => {
let mockFn = jest.fn().mockReturnValue('name');
expect(mockFn()).toBe('name');
})
//定义jest.fn()的内部实现并断言其结果
it('jest.fn()的内部实现', () => {
let mockFn = jest.fn((a, b) => {
return a + b;
})
expect(mockFn(2, 2)).toBe(4);
})
//jest.fn()返回Promise对象
it('jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('name');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为name
expect(result).toBe('name');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
- jest.mock() — jest.mock автоматически упорядочивает фиктивные объекты в соответствии с издеваемым модулем. Мок-объект будет иметь поля и методы исходного модуля.
// kAuthCode.spec.js
jest.mock('@/service/modules/login.js', () => ({
getPhoneCode: () => Promise.resolve({
data: {
answer: 'mock_yes',
image: 'mock.png',
}
})
}))
it('设置手机号码', () => {
const getCode = jest.fn();
option.methods = {getCode};
wrapper.find('.kauthcode_btn').trigger('click');
expect(getCode).toHaveBeenCalled()
expect(wrapper.vm.phone).toBe(testPhone);
});
Вам нужно имитировать весь запрос axios и использовать toHaveBeenCalled, чтобы определить, вызывается ли этот метод.
В этом примере нам нужно сосредоточиться только на методе getCode, а другие можно игнорировать. Чтобы протестировать этот метод, мы должны сделать:
- Нам не нужно на самом деле вызывать метод axios.get, нам нужно имитировать его
- Нам нужно проверить, вызывается ли метод axios (но не срабатывает ли он на самом деле) и возвращается ли Promise
- Возвращенный объект Promise выполняет функцию обратного вызова
Примечание: Иногда возникает ситуация, когда один и тот же метод вызывается в одном и том же компоненте, но возвращаемое значение отличается. Возможно, нам придется несколько раз имитировать его. В это время нам нужно использовать метод restoreAllMocks в beforeEach для сброса состояние.
Цель макета:
- установить возвращаемое значение функции
- Получить статус вызова функции
- Изменить внутреннюю реализацию исходной функции
4. ️ Ступайте на яму 🏆
1.触发事件 - 假设组件库使用的是iview中对<Checkbox>提供的@change事件,但是当我们进行
wrapper.trigger('change')时,是触发不了的。<Button>的@click()和<button>的@click也是有区别的。
2。渲染问题 - 组件库提供的组件渲染后的html,需要通过wrapper.html()来看,可能会与你从控
制台看到的html有所区别,为避免测试结果出错,还应console.log一下wrapper.html()看一下实际的渲染结果