Юнит-тестирование Jest для фронтенд-тестирования

Vue.js
Юнит-тестирование Jest для фронтенд-тестирования

1. Введение в шутку

  1. Преимущество: Высокая скорость, простой API, простая конфигурация
  2. Приставка: Jest не поддерживает синтаксис модуля ES, необходимо установить Babel
npm install -D @babel/core @babel/preset-env

.babelrc

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

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

npx jest --init
  1. шуточный режим
  • jest --watchAll: Когда изменения в тестовом файле будут найдены, снова запустите все тестовые файлы.
  • jest --watch: требует иgitПри использовании в комбинации будет сравниваться разница между существующим файлом и зафиксированным файлом, и будет тестироваться только файл разницы.

2. Сопоставитель шуток

Общие сопоставители

  • toBe
  • toEqual: суждениеобъектСодержимое равно
  • toMatchObject: expect(obj).toMatchObject(o), ожидать, что o будет содержать obj
  • toBeNull
  • toBeUndefined
  • toBeDefinded
  • toBeTruthy
  • toBeFalsy
  • not: для отрицания, например .not.toBeTruthy()

Связанный номер

  • toBeGreaterThan (больше) / toBeGreaterThanOrEqual (больше или равно)
  • toBeCloseTo: используется для сравнения чисел с плавающей запятой, утверждение выполняется, когда они примерно равны.
  • toBeLessThan / toBeLessThanOrEqual

Связанные строки

  • toMatch: Параметру может быть передана строка или обычный

Связанный набор массивов

  • toContain

сопоставитель исключений

  • бросать:
const throwError = () => {
  throw new Error('error')
}

it('can throw error', () => {
  expect(throwError).toThrow('error') // 判断throw函数可以抛出异常,异常信息为 "error"。也可以写正则
})

Вот маленькая хитрость: когда мы хотимИгнорировать другие тестовые наборы в одном файле и ориентироваться только на один тестовый наборПри отладке можно добавить.only

it.only('test', () => {
  // ...
})

но это не игнорирует тестовые случаи из других тестовых файлов

3. Протестируйте асинхронный код

Вот три асинхронных метода, протестируйте код этих трех",Woohoo. Dell-Lee.com/react/API/…" вернет {успех: правда}, "Уууу. Dell-Lee.com/react/API/4…" не существует.

import axios from 'axios'

export function getData1() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

export function getData2(fn) {
  axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
    fn(res)
  })
}

export function get404() {
  return axios.get('http://www.dell-lee.com/react/api/404.json')
}

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

  1. done, который контролирует время окончания тестового примера
  2. Если возвращаемое значение выполнения функции является обещанием, верните обещание
  3. async + await
import {getData1, getData2, get404} from './fetchData/fetchData'

it('getData1 方法1', (done) => {
  getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
    done()  // 如果不加 done,还没执行到 .then 方法,测试用例已经结束了
  })
})

it('getData1 方法2', () => {
  return getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
  })
})

it('getData2 方法2', (done) => {
  getData2((res) => {
    expect(res.data).toEqual({
      success: true
    })
    done()
  })
})

it('getData1 方法3', async () => {
  const res = await getData1()
  expect(res.data).toEqual({
    success: true
  })
})

/*********** 重点关注 ***********/
it('get404', (done) => {
  expect.assertions(1)
  get404().catch(r => {
    expect(r.toString()).toMatch('404')
    done()
  })
})

Сосредоточьтесь на последнем тестовом примере выше. Предположим, что теперь у нас есть интерфейс, который возвращает 404. Нам нужно протестировать этот интерфейс и ожидать, что он вернет 404. Ловим уловом и судим по уловке.

Однако если интерфейс вернет 200 вместо 404, перехват не будет выполнен, ожидание не будет выполнено, а тест все равно будет пройден. Это не то, что мы ожидали! Итак, нам нужно добавитьexpect.assertions(1)Сделайте утверждение:Следующее обязательно выполнит ожидание

Конечно, вы также можете использовать асинхронный метод ожидания для тестирования интерфейса 404.

it('get404 方法3', async () => {
  await expect(get404()).rejects.toThrow()
})

4. Некоторые хуки в Jest

  • BEFORELL: перед использованием случаев начните выполнять
  • beforeEach: перед выполнением каждого варианта использования
  • afterEach
  • afterAll
  • describe

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

beforeAll(() => {
  // ...
})

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

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

describe('测试 Button 组件', () => {
  beforeAll(...)  // 1
  beforeEach(...) // 2
  afterEach(...)  // 3
  afterAll(...)   // 4

  describe('测试 Button 组件的事件', () => {
    beforeAll(...)  // 5
    beforeEach(...) // 6
    afterEach(...)  // 7
    afterAll(...)   // 8
    it('event1', ()=>{...})
  })
})

Порядок выполнения вышеприведенной функции ловушки таков: 1 > 5 > 2 > 6 > 3 > 7 > 4 > 8
Внешняя функция ловушки также влияет на вариант использования внутри описания, и порядок выполнения следующий:Сначала внешний, потом внутренний

5. Насмешка в шутку

1. Моделирование асинхронных методов в Jest

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

  1. Наш метод запроса экспортируется в mock.js
import axios from 'axios'

export function getData() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
  1. Создайте mock.js в том же каталоге, что и mock.js.mocksПапка, создайте файл с соответствующим именем файла в папке, этот файл является методом экспорта является методом имитации запроса
    1580732355(1)
    Здесь мы возвращаем обещание напрямую и разрешаем поддельные данные.
export function getData() {
  return Promise.resolve({
    success: true
  })
}
  1. Часть тестового примера: Вот заметкаяма:jest.mock не может быть записан ни в какую функцию ловушки, из-за тайминга выполнения функции-хука, и не работает beforeAll.При выполнении функции-хука был выполнен код, не написанный в функции-хуке, то есть он был импортирован!
jest.mock('./mock/mock.js')  // 声明下面引入的 getData 方法是 jest 模拟的,如果不需要引入该方法则不需要声明

import {getData} from './mock/mock.js'  // 导入 mock.js,但实际上 jest 会导入 __mocks__ 下的 mock.js

test('mock 方法测试', () => {
  getData().then(data => {
    expect(data).toEqual({
      success: true
    })
    done()
  })
  
})

В дополнение к вышеописанному способу вы также можете настроить автоматическое открытие макета в jest.config.js, чтобы jest автоматически определял, находится ли текущий файл на том же уровне.mockпапка, есть ли в ней соответствующий файл?

module.exports = {
  automock: true
}

Есть два способа насмешки, и есть крайний случай, чтобы избежать насмешки:
Мы определяем метод getData, который требует насмешек в mock.js, и еще один распространенный метод, который не требует насмешек.Когда мы импортируем тестовый файл, нам нужно избегать шутокmocksНайдите этот общий метод в /mock.js, куда вам нужно импортировать его с помощью метода, предоставляемого jest:

const { regularMethod } = jest.requireActual('./mock/mock.js')

2. Управление временем с помощью Jest

Когда у нас есть следующий код для тестирования:

export default (fn) => {
  setTimeout(() => {
    fn()
  }, 3000)
}

Мы не всегда можем дождаться таймера, в это время мы используем Jest для управления временем! Действуйте следующим образом:

  1. пройти черезjest.useFakeTimers()использовать шутку"Домашнее"Таймер помещен здесь перед каждым, потому чтоВремя перемотки вперед может вызываться несколько раз, я надеюсь, что в каждом тестовом примере эти часы являются начальным состоянием и не будут влиять друг на друга.
  2. После выполнения функции таймера перемотайте время вперед на 3 секунды.jest.advanceTimersByTime(3000), этот метод можно вызывать любое количество раз, и время перемотки вперед будет наложено.
  3. В это время мы переключились на 3 секунды, и ожидаем, что они также могут подействовать!

Специальное примечание: jest.fn() генерирует функцию, эта функцияМожет вызываться слушателем несколько раз.

import timer from './timer/timer'

beforeEach(() => {
  jest.useFakeTimers()
})

it('timer 测试', () => {
  const fn = jest.fn()
  timer(fn)
  jest.advanceTimersByTime(3000)
  expect(fn).toHaveBeenCalledTimes(1)
})

3. Мок-класс

Точно так же, когда нас интересует только то, вызывается ли метод класса, а не результат вызова метода, мы можем имитировать класс

Класс Util определен в util/util.js

export class Util {
  a() {}
  b() {}
}

Этот класс вызывается в util/useUtil

import {Util} from './util'

export function useUtil() {
  let u = new Util()
  u.a()
  u.b()
}

Нам нужно проверить, что u.a и u.b вызываются,jest.mock('./util/util') Mock Util, Util.a, Util.b в jest.fn

Тестовый пример выглядит следующим образом:

jest.mock('./util/util')  // mock Util 类
import {Util} from './util/util'
import {useUtil} from './util/uesUtil'

test('util 的实例方法被执行了', () => {
  useUtil()
  expect(Util).toHaveBeenCalled()
  expect(Util.mock.instances[0].a).toHaveBeenCalled()
  expect(Util.mock.instances[0].b).toHaveBeenCalled()
})

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

1. Начало работы с простыми вариантами использования

Vue предоставляет @vue/test-utils, чтобы помочь нам с модульным тестированием.При создании проекта Vue проверка опции теста автоматически установит его для нас.

Давайте сначала представим два широко используемых метода монтажа:

  • mount: смонтирует компонент и подкомпоненты, содержащиеся в компоненте.
  • мелкое монтирование: поверхностное монтирование, монтируются только компоненты, игнорируя подкомпоненты.

Возьмем простой тестовый пример:

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

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

мелкое монтирование вернет обертку, эта обертка будет содержать множество методов, которые помогут нам в тестировании,смотрите подробности

2. Снимок теста

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

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

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

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

1580787793(1)
Можно видеть, что на самом деле моментальный снимок сохраняет html-часть после рендеринга компонента, css-часть не сохраняется, и некоторые события, такие как @click, привязанные к элементу, также не сохраняются. Так что снимки подходят дляТест, чтобы увидеть, изменился ли узел DOM.

Когда снимок изменится, мы можем нажатьuСделать снимок обновления

1580788050(1)

3. Тестирование покрытия

Тест покрытия — это оценка полноты теста.Чем больше бизнес-кода охватывает тест, тем выше охват.

В jest.config.js мы можем установитьcollectCoverageFrom, для установки файлов, которые необходимо протестировать на покрытие, здесь мы тестируем все.vueфайл, игнорироватьnode_modulesСкачать все файлы.

Обратите внимание, что для настройки jest в Vue см.Документация

Затем добавьте команду скрипта для проверки:

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

Выполнение команды сгенерируетcoverageпапка,Icov-report/index.htmlбудет визуализировать наше тестовое покрытие

4. Протестируйте с помощью Vuex

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

import store from '@/store/index'

const wrapper = mount(HelloWorld, {
    store
})

7. Пишите в конце

1. Модульное тестирование или интеграционное тестирование?

просто возьмиshallowMountНапример, этот API очень подходит для юнит-тестирования, модульное тестирование не фокусируется на связи между юнитами, а тестирует каждый юнит отдельно. Это также делает егоОбъем кода велик, а тестовые комнаты слишком независимы.. При тестировании некоторых библиотек функций, когда каждая функция относительно независима, это очень удобно для модульного тестирования.
При тестировании некоторых бизнес-компонентов нужно обращать внимание на связь между компонентами, что больше подходит для интеграционного тестирования.

2. TDD или BDD?

TDD: разработка через тестирование. Сначала напишите тестовые примеры, а затем напишите код в соответствии с вариантами использования, уделяя больше внимания самому коду. следующим образом:

describe('input 输入回车,向外触发事件,data 中的 inputValue 被赋值', () => {
  const wrapper = shallowMount(TodoList)
  const inputEle = wrapper.find('input').at(0)
  const inputContent = '用户输入内容'
  inputEle.setValue(inputContent)
  // expect:add 事件被 emit
  except(wrapper.emitted().add).toBeTruthy()
  // expect:data 中的 inputValue 被赋值为 inputContent
  except(wrapper.vm.inputValue).toBe(inputContent)
})

TDD обращает внимание на то, как код реализован внутри, и срабатывает ли событие? Свойство установлено? данные Данные обновляются?

BDD: разработка, основанная на поведении пользователя, сначала напишите бизнес-код, а затем протестируйте с точки зрения пользователя.Функция, не обращайте внимание на процесс реализации кода, простоТестируйте функциональность, имитируя действия пользователя.
Например, следующий вариант использования:

describe('TodoList 测试', () => {
  it(`
    1. 用户在 header 输入框输入内容
    2. 键盘回车
    3. 列表项增加一项,内容为用户输入内容
  `, () => {
    // 挂载 TodoList 组件
    const wrapper = mount(TodoList)
    // 模拟用户输入
    const inputEle = wrapper.find('input').at(0)
    const inputContent = '用户输入内容'
    inputEle.setValue(inputContent)
    // 模拟触发的事件
    inputEle.trigger('content')
    inputEle.trigger('keyup.enter')
    // expect:列表项增加对应内容
    const listItems = wrapper.find('.list-item')
    expect(listItems.length).toBe(1)  // 增加 1 项
    expect(listItems.at(0).text()).toContain(inputContent)  // 增加 1 项
  })
})

Ссылаться на:

Тестовые уроки, которые нужно усвоить на переднем крае