интерфейсное модульное тестирование

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

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

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()看一下实际的渲染结果

🚀Конфигурация официального сайта Vue Test Utils