Изучите тестирование приложений Vue, чтобы сделать ваш проект более надежным и стабильным

Vue.js
Изучите тестирование приложений Vue, чтобы сделать ваш проект более надежным и стабильным

Тестирование приложения Vue

Эта статья подытоживает чтение книги "Тест приложений Vue.js", знакомство с официальным сайтом "Vue Test Utils" и практический опыт работы. Пожалуйста, поддержите подлинную версию книги.
Если вы считаете, что это хорошо написано, пожалуйста, перейдите на мойGithubдайте мне одинstar

Введение в тест

Фронтенд-приложения в основном пишут три типа тестов:модульный тест,снимок теста,сквозное тестирование. Эта статья посвященаVueкомпонентмодульный тестиснимок теста,засквозное тестированиеПожалуйста, ищите соответствующий контент самостоятельно.

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

Покрытие кода — важный показатель качества приложения или библиотеки, обычно0%Указывает, что тестирование кода не проводилось,100%Это означает, что каждая строка кода выполняется при выполнении тестового примера.100%Покрытие может быть одинаковым0%Освещение также пугает, потому что оно может дать вам иллюзию того, что ваша программа никогда не пойдет не так, как надо, тогда как на самом деле, скорее всего, вы неверно оценили сцену и пришли к неверному выводу. Например: когда вы тестируетеAPIинтерфейс, вы предполагаете, чтоAPIИнтерфейс никогда не возвращает сообщение об ошибке, однако, когдаAPIИнтерфейс находится в формальной среде и возвращает сообщение об ошибке.

Для полного приложения вы можете получить тестовое покрытие за долю времени80%90%, однако остальные10%20%Это также может занять у вас в несколько раз больше времени, или даже не достичь его вообще. Точно так же, как вы можете легко выжать большую часть воды из полотенца, выжать оставшуюся маленькую часть все еще очень сложно.

существуетVueзаявка наUIДля компонентов мы не рекомендуем слепо следовать охвату на уровне строк, потому что чрезмерная сосредоточенность на покрытии может привести к тому, что мы будем уделять слишком много внимания деталям реализации внутри компонента, что приведет к слишком большому количеству утомительных тестов. Вместо этого мы хотим написать тесты компонентов, чтобы подтвердить общедоступный интерфейс компонента и обрабатывать его в черном ящике, простойUIТестовый пример компонента будет подтверждать некоторые входные данные (Взаимодействие с пользователемилиВход реквизитаи т. д.) к компоненту и ожидать, что компонент получит ожидаемый результат (визуализировать результатилиреагировать на событияЖдать).

сквозное тестирование

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

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

function testCalc (browser) {
  browser.url('http://localhost:8080')
         .click('#button')
         .click('#button-plus')
         .click('#button')
         .assert.containsText('#result', '2')
         .end();
}

Мы можем подвести итог из приведенного выше небольшого случая:

преимущество:

  • Написание тестовых случаев значительно экономит время.
  • Не стесняйтесь настраивать его в соответствии с вашими потребностями.

недостаток:

  • Тестовые случаи могут выполняться не очень быстро: запуск браузера может занять несколько секунд, и время зависит от скорости веб-сайта.
  • Сквозная отладка может быть затруднена: в приведенном выше примере отладка в локальной среде — это плохой процесс, и это может привести к сбою, если тест не пройден на сервере непрерывной интеграции.
  • сквозной может статьflakyконтрольная работа:flakyТест означает, что даже если тестируемое приложение работает правильно, тест все равно часто дает сбой, и причина сбоя зависит от множества факторов: слишком долгое время выполнения кода или временно неработающий API.

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

Модульное тестирование — это процесс запуска тестов на самых маленьких частях приложения. Как правило, единицей измерения является функция, но вVueВ приложении компонент также является тестируемым модулем.

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

// math.js
export function add (a, b) {
  return a + b
}

// math.spec.js
import { add } from 'math.js'
describe('math.js', () => {
  it('add func', () => {
    expect(add(1, 2)).toBe(3)
  })
})

Подведем итоги модульного тестирования:

преимущество:

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

недостаток:

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

снимок теста

Тест моментальных снимков делает «фотографию» работающего приложения, сравнивает ее с ранее сохраненной «фотографией» и не проходит тест, если они не совпадают. После этого мы будем использоватьJestсреда автоматизированного тестированияVueКомпонент для тестирования снапшотов.

Установить

Официальный репозиторий автоматизированных тестов

если вы просто хотите учитьсяvue-test-utils, тогда вы можете попробовать минималистичный тестовый репозиторий, предоставленный официальным.

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

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

|-- vue-test-utils-getting-started
|   |-- .babelrc      # babel配置
|   |-- .gitignore    # git配置
|   |-- counter.js    # 组件
|   |-- test.js       # 测试文件
|   |-- package.json
|   |-- README.md

Уведомление: Следует отметить, что этот метод может напрямую не поддерживать наше использование.vueфайловая форма, если требуется для поддержки.vueфайл, нам нужно выполнить дополнительную настройку.

Установка с использованием официальных лесов

Что, если выVue-Cliлюбитель , то поVue-Cli4.0+Вы можете быстро создать содержащуюtestПротестируйте соответствующую конфигурацию проекта.

# 创建项目
$ vue create vue-jest

# 选择自定义配置
  default (babel, eslint)  
> Manually select features 

# 选择安装feature,也可以根据自己的喜好去安装
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support        
 ( ) Router
 ( ) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
>(*) Unit Testing
 ( ) E2E Testing

 # 选择测试框架,我们选择jest
   Mocha + Chai
> Jest

После завершения установки появитсяtestsкаталог, который содержит простой тестовый примерexample.spec.js: ::: Подсказка можно использоватьnpm run test:unitдля запуска тестового примера. :::

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.text()).toMatch(msg)
  })
})

Добавьте тесты Jest в существующие проекты Vue.

Если вы находитесь в существующем проекте Vue, вам нужно добавитьJestСреда автоматизированного тестирования, вы можете запускать в проекте следующие команды:

# 请确保你的Vue-Cli安装了最新版本
$ vue add unit-jest

После выполнения этой команды она автоматически поможет нам установить@vue/cli-plugin-unit-jest, и поможет намjestПротестируйте соответствующую конфигурацию, и это также поможет нам создать новую в корневом каталоге.testsпапка, содержащая тестовые случаиexample.spec.js: ::: Подсказка можно использоватьnpm run test:unitдля запуска тестового примера. :::

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.text()).toMatch(msg)
  })
})

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

Плагин редактора VsCode

Для использованияVsCodeредактор, который можно установитьJestплагин, который можно запустить, когда мы неnpm run test:unitКоманда запрашивает, проходит ли тестовый пример.

Тестовое покрытие и отчеты о тестировании

в корневом каталогеjest.config.js, настраиваем следующим образом:

module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
  snapshotSerializers: ['jest-serializer-vue'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testMatch: [
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  collectCoverage: true,
  coverageDirectory: '<rootDir>/tests/unit/coverage',
  collectCoverageFrom: [
    'src/components/**/*.vue',
    'src/utils/**/*.ts',
    'src/store/modules/*.ts',
    '!src/utils/axios.ts',
    '!src/utils/notify.ts'
  ]
}

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

  • snapshotSerializers: VueкомпонентыJestКонфигурация инструмента для сериализации моментальных снимков.
  • moduleNameMapper: Конфигурация псевдонима модуля.
  • testMatch: Тестовые правила поиска файлов, которые можно разместить в единомsrc/testsкаталог, или его можно разместить рядом__tests__Под содержанием.
  • collectCoverage: следует ли выполнять сбор тестового покрытия.
  • coverageDirectory: место, где хранится отчет о тестировании.
  • collectCoverageFrom: какие файлы тестировать, а какие нет, вы можете установить в соответствии с вашей командой или личными предпочтениями.

После завершения вышеуказанной настройки вы можете запустить в терминалеnpm run test:unitкоманда, то вы получите тестовый отчет, подобный следующему:

В приведенном выше отчете о тестировании мы явно видим, что есть некоторые коды, которые не покрыты.В настоящее время мы можемsrc/tests/units/coverage/lcov-reportНайдите в каталоге соответствующий тестовый файл и щелкните его. здесь сbase/scroll/index.vueНапример, у него есть следующие два файла:

  • index.html: вот правильноscroll/index.vueСводный отчет о тестировании компонентов выглядит следующим образом:

  • index.vue.html: вот правильноscroll/index.vueПодробное описание тестирования компонентов, в котором код, который не охвачен, помечен разными цветами, как показано ниже:

Вывод рендеринга тестового компонента

крепление компонента

Для компонентов без подкомпонентов используйтеshallowMountиmountЭффект тот же для компонентов. Разница между ними в том, что,shallowMountВизуализирует только сам компонент, но сохраняет заглушки дочерних компонентов в компоненте.

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

После того, как компонент смонтирован, мы можем пройтиwrapper.vmДоступ к экземпляру компонента черезwrapper.vmИ тогда вы можете получить доступ ко всем компонентамprops,dataиmethodsи Т. Д.

Уведомление:в нашем использованииmountилиshallowMount, мы можем ожидать, что компонент будет реагировать почти на всеVueФункции жизненного цикла, но если они не вызываются вручнуюwrapper.destory()функция, в противном случае компонентbeforeDestroy()иdestroyed()не сработает.

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

describe('HelloWorld.vue', () => {
  it('test shallowMount', () => {
    const wrapper = shallowMount(HelloWorld)
    // 判断组件是否挂载
    expect(wrapper.exists()).toBe(true)
    
    // 访问vm实例
    console.log(wrapper.vm)
  })
  it('test mount', () => {
    const wrapper = mount(HelloWorld)
    // 判断组件是否挂载
    expect(wrapper.exists()).toBe(true)
  })
})

Тест рендеринга текста

После монтирования компонента возвращаемая оболочка имеетwrapper.text()метод, который возвращает отображаемое текстовое содержимое компонента.

Предположим, у вас есть следующие компоненты:

<template>
 <div>{{msg}}</div>
</template>
<script>
export default {
  data () {
    return {
      msg: 'Hello, Vue and Jest...'
    }
  }
}
</script>

Тогда мы можем написать следующий тестовый пример:

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

describe('HelloWorld.vue', () => {
  it('test text', () => {
    const msg = 'Hello, Vue and Jest...'
    const wrapper = shallowMount(HelloWorld)
    // 更推荐具有扩展性的toContain匹配器而不是toBe
    expect(wrapper.text()).toBe(msg)       // 严格相等
    expect(wrapper.text()).toContain(msg)  // 是否包含
  })
})

Проверка структуры рендеринга HTML

После монтирования компонента возвращаемая оболочка имеетwrapper.html()метод, который возвращает обработанную DOM-структуру компонента.

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

<template>
  <div>
    <span class="item">item</span>
    {{msg}}
  </div>
</template>
<script>

export default {
  data () {
    return {
      msg: 'Hello, Vue and Jest...'
    }
  }
}
</script>

Тогда мы можем написать следующий тестовый пример:

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

describe('HelloWorld.vue', () => {
  it('test html', () => {
    const wrapper = shallowMount(HelloWorld)
    expect(wrapper.html()).toContain('<span class="item">item</span>')
  })
})

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

После монтирования компонента возвращаемая оболочка имеетwrapper.attributes()метод, который возвращает объект атрибута DOM после рендеринга компонента,attributes()Если метод предоставляет параметр имени атрибута, он напрямую возвращает значение этого атрибута, в противном случае он возвращает объект всех атрибутов,wrapper.classes()Метод аналогичен.

все еще вышеHelloWorld.vueкомпонент в качестве примера, если мы хотим протестироватьspanЕсть ли на этикетке.itemстиль, естьid, могут быть выполнены следующие тесты:

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

describe('HelloWorld.vue', () => {
  it('test attribute and class', () => {
    const wrapper = shallowMount(HelloWorld)
    // 查找第一个span标签
    const dom = wrapper.find('span')
    expect(dom.classes()).toContain('item')
    expect(dom.attributes().id).toBeFalsy()
  })
})

Тест реквизита

После монтирования компонента возвращаемая оболочка имеетwrapper.props()метод, который возвращает всеprops,props()Если метод предоставляет параметр, он возвращает значение этого параметра напрямую, в противном случае он возвращает все.

МыHelloWorld.vueКомпоненты вносят дополнительные коррективы:

<template>
  <div>
    <span class="item">item</span>
    {{msg}}
    <span>{{name}}</span>
    <span>{{age}}</span>
  </div>
</template>
<script>

export default {
  props: ['name', 'age'],
  data () {
    return {
      msg: 'Hello, Vue and Jest...'
    }
  }
}
</script>

Затем пишем следующий тестовый пример:

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

describe('HelloWorld.vue', () => {
  it('test props', () => {
    const wrapper = shallowMount(HelloWorld, {
      propsData: {
        name: 'AAA',
        age: 23
      }
    })
    expect(wrapper.props('name')).toBe('AAA')
    expect(wrapper.props().age).toBe(23)
  })
})

Тест стиля

Возвращенная оболочка содержитelementимущество, которое является текущимDOMПриложения Node могут использоватьelement.styleпосетитьDOMузлавстроенный стиль.

Предположим, у нас естьHelloWorld.vueВнесите следующие изменения:

<template>
  <div class="hello">
    <h1 style="width: 100px;height: 50px;">{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

На основе вышеперечисленных компонентов мы можем написать следующие тестовые примеры:

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

describe('HelloWorld.vue', () => {
  it('test style', () => {
    const wrapper = shallowMount(HelloWorld)
    const style = wrapper.find('h1').element.style
    expect(style.width).toBe('100px')
    expect(style.height).toBe('50px')
  })
})

метод тестовых компонентов

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

частные и публичные методы

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

Правила тестирования:

  • Частные методы: Частные методы обычно вызываются только внутри компонента.Они являются деталями реализации внутри компонента.Вообще говоря, вам не нужно писать тестовые примеры для него, но это не абсолютно.
  • Публичные методы: публичные методы будут вызываться внешними компонентами, поэтому вам нужно написать тестовые примеры для публичных методов.

Предположим, у нас есть следующееloading.vueКомпоненты:

<template>
  <div v-show="showLoading" class="loading-box">
    {{loadingText}}
  </div>
</template>

<script>
export default {
  data () {
    return {
      loadingText: '',
      showLoading: false
    }
  },
  methods: {
    show () {
      this.showLoading = true
      this.getLoadingText()
    },
    hide () {
      this.showLoading = false
      this.getLoadingText()
    },
    getLoadingText () {
      this.loadingText = this.showLoading ?  `loading show text` : ''
    }
  }
}
</script>

Компонентный метод анализа:

  • Частный метод:getLoadingText(), нам не нужно писать для него тестовые примеры.
  • общедоступный метод:show(),hide(), нам нужно написать тестовые примеры для него.

Затем, в соответствии с приведенными выше правилами, мы можем написать тестовый пример:

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

describe('loading.vue', () => {
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(Loading)
  })
  it('show func', async () => {
    expect(wrapper.vm.loadingText).toBe('')
    expect(wrapper.vm.showLoading).toBe(false)
    expect(wrapper.isVisible()).toBe(false)

    wrapper.vm.show()
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.showLoading).toBe(true)
    expect(wrapper.isVisible()).toBe(true)
  })

  it('hide func', async () => {
    wrapper.setData({
      showLoading: true
    })
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.showLoading).toBe(true)
    expect(wrapper.isVisible()).toBe(true)

    wrapper.vm.hide()
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.showLoading).toBe(false)
    expect(wrapper.isVisible()).toBe(false)
  })
})

Анализ тестового кода:

  • beforeEach():этоJestФункция-ловушка будет вызываться перед выполнением каждого тестового примера.В этой функции-ловушке мы перемонтируем компонент, чтобы избежать влияния нескольких тестовых случаев друг на друга.
  • async/await: поскольку мы вызываем общедоступный метод, он изменяет данные компонента, что, в свою очередь, вызываетDOMupdate, поэтому нам нужно вызвать компонент$nextTick()метод, чтобы убедиться, что мы получаем правильныйDOMстатус.
  • isVisible(): судитьDOMвиден ли элемент, вloadingкомпонент, потому что мы используемv-showдиректива, поэтому используйтеisVisible()Чтобы быть более семантичным, мы также можем использоватьexists()иv-ifВместо этого инструкция.
  • setData(): Вручную изменить компонентdataзначение, использование такое же, какVue.set()похожий. Однако, когда вам нужно обратить внимание,setData()Метод асинхронный и требует сотрудничества$nextTick()использовать вместе.

Проверьте функцию таймера

Используйте фальшивую функцию таймера

существуетJavaScriptФункция таймера посередине имеетsetTimeoutиsetInterval, если мы не имеем дело с функцией таймера, когда компонент имеет задержку1000msизsetTimeout, это означает, что наша тестовая программа должна ждать1000ms, если их многоsetTimeoutфункции, то это, несомненно, катастрофа для модульного тестирования со скоростью и эффективностью.

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

setTimeout = () => { console.log('replace setTimeout') }

мы можем использоватьJestпредоставлена ​​библиотекаjest.useFakeTimers(), когда этот метод вызываетсяJestПредоставленный поддельный таймер заменяет функцию глобального таймера для работы, тогда мы можем использоватьjest.runTimersToTime()опережать время.

Предположим, у нас есть следующие компоненты:

<template>
  <div class="hello">
    {{timeText}}<br/>
    {{percent}}
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      percent: 0,
      timeText: ''
    }
  },
  methods: {
    start () {
      this.percent = 0
      this.timer = setInterval(() => {
        this.percent++
        if (this.percent >= 100) {
          this.finish()
        }
      }, 100)
    },
    finish () {
      this.percent = 100
      clearInterval(this.timer)
    }
  },
  mounted () {
    setTimeout(() => {
      this.timeText = 'setTimeout text'
    }, 1000)
  }
}
</script>

Тогда мы можем написать следующий тестовый пример:

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

describe('HelloWorld.vue', () => {
  let wrapper
  beforeEach(() => {
    jest.useFakeTimers()
    wrapper = shallowMount(HelloWorld)
  })
  it('test setTimeout async timer', () => {
    expect(wrapper.vm.timeText).toBe('')
    jest.runTimersToTime(1000)
    expect(wrapper.vm.timeText).toBe('setTimeout text')
  })
  it('test setInterval async timer', () => {
    expect(wrapper.vm.percent).toBe(0)
    wrapper.vm.start()
    jest.runTimersToTime(100)
    expect(wrapper.vm.percent).toBe(1)
    jest.runTimersToTime(900)
    expect(wrapper.vm.percent).toBe(10)
    jest.runTimersToTime(2000)
    expect(wrapper.vm.percent).toBe(30)
  })
})

Анализ тестового кода:

  • beforeEach: потому что мы используем в обоих тестовых случаяхJestпредоставляет фальшивую функцию таймера, поэтому мы можемbeforeEachФункция хука для использования. Точно так же мыbeforeEachПеремонтируйте компоненты в функции ловушки, чтобы избежать влияния нескольких тестов друг на друга.
  • jest.runTimersToTime(): означает время опережения, во втором случае использования: когда первое время опережения100msчас,percentЗначение равно 1; когда второе время опережения900ms, на этот раз засчитывается первый аванс100ms, в общей сложности1000ms,следовательноpercentЗначение равно 10.

Тест со шпионом

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

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

  • как проверитьclearInterval()функция выполняется.
  • как проверитьclearInterval()переносимые параметры.

Чтобы решить первую проблему выше, нам нужно использоватьJestкоторый предоставилjest.spyOn()функция, тоwindow.clearInterval()Выполнить шпионский подлог следующим образом:

jest.spyOn(window, 'clearInterval')

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

setInterval.mockReturnValue(996)

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

it('clearInterval success when percent >= 100', () => {
  jest.spyOn(window, 'clearInterval')
  setInterval.mockReturnValue(996)
  wrapper.vm.start()
  wrapper.vm.finish()
  expect(wrapper.vm.percent).toBe(100)
  expect(window.clearInterval).toHaveBeenCalledWith(996)
})

Анализ тестового кода: когда наш тестовый код использует вещи, которые мы не можем контролироватьAPI, мы можем использоватьspyчтобы замаскировать, а затем судить нашу маскировкуAPIназывается.

фиктивный код

существуетVueв разработке, дляVueЭто распространенный способ добавления некоторых свойств или методов к экземпляру, например:

import { Message } from 'element-ui'
Vue.prototype.$message = Message

this.$message.success('保存成功')

Так как же нам добавить модульные тесты для этих свойств экземпляра? ответmocks, это может бытьVueЭкземпляры предоставляют дополнительные свойства. Если у нас есть следующееmessage.vueКомпоненты:

<template>
  <div>
    <button id="success" @click="handleSuccessClick">成功</button>
    <button id="warning" @click="handleWarningClick">警告</button>
    <button id="error" @click="handleErrorClick">错误</button>
    <button id="info" @click="handleInfoClick">消息</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleSuccessClick () {
      this.$message.success('成功')
    },
    handleWarningClick () {
      this.$message.warning('警告')
    },
    handleErrorClick () {
      this.$message.error('错误')
    },
    handleInfoClick () {
      this.$message.info('消息')
    }
  }
}
</script>

Потому что мы собираемсяmockПлагины от третьих лиц, поэтому нам не нужно их устанавливать в наших тестовых примерах.element-ui, поэтому мы можем написать следующий тестовый пример:

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

describe('message.vue', () => {
  it('add mocks', () => {
    const message = {
      success: jest.fn(),
      warning: jest.fn(),
      error: jest.fn(),
      info: jest.fn()
    }
    const wrapper = shallowMount(Message, {
      mocks: {
        $message: message
      }
    })
    const successBtn = wrapper.find('#success')
    const warningBtn = wrapper.find('#warning')
    const errorBtn = wrapper.find('#error')
    const infoBtn = wrapper.find('#info')

    successBtn.trigger('click')
    expect(message.success).toHaveBeenCalledTimes(1)

    warningBtn.trigger('click')
    expect(message.warning).toHaveBeenCalledTimes(1)

    errorBtn.trigger('click')
    expect(message.error).toHaveBeenCalledTimes(1)

    infoBtn.trigger('click')
    expect(message.info).toHaveBeenCalledTimes(1)
  })
})

Зависимости фиктивного модуля

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

Если у нас есть следующееHelloWorld.vueкомпоненты

<template>
  <ul>
    <li
      v-for="(item, index) in lessonList"
      :key="index"
      class="lesson-item"
    >{{item.title}}</li>
  </ul>
</template>

<script>
import { getLessonList } from '@/api/api.js'
export default {
  data () {
    return {
      lessonList: []
    }
  },
  methods: {
    getLessonData () {
      getLessonList().then(res => {
        const { status, data } = res
        if (status === 200) {
          this.lessonList = data.data
        }
      })
    }
  },
  mounted () {
    this.getLessonData()
  }
}
</script>

новыйsrc/api/api.jsфайл и добавьте следующий код:

import axios from 'axios'

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

анализ проблемы:

  • Шаг 1: Мы должны решитьgetLessonListметод возвращает проблему с поддельными данными.
  • Шаг 2: Мы должны решитьHelloWorld.vueКомпонент правильно отображает наши поддельные данные.

Для первого вопроса нам нужно создать новыйsrc/api/__mocks__/api.jsпапка, примечание:

  • __mocks__является фиксированным правописанием, это может бытьJestидентифицировать.
  • __mocks__/api.js__mocks__Имя файла в каталоге должно совпадать с именем файла модуля, который мы моделируем.
export const getLessonList = jest.fn(() => {
  const lessonResult = {
    success: true,
    data: [
      { id: 1, title: '深入理解ES6' },
      { id: 2, title: 'JavaScript高级程序设计' },
      { id: 3, title: 'CSS揭秘' },
      { id: 4, title: '深入浅出Vue.js' }
    ]
  }
  return Promise.resolve(lessonResult)
})

Второй вопрос, мы можем использоватьgetLessonList.mockResolvedValueOnce()метод для передачи наших фиктивных данных. После решения вышеуказанных задач мы можем написать следующие тестовые примеры:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import { getLessonList } from '../../src/api/api.js'
jest.mock('../../src/api/api.js')
describe('HelloWorld.vue', () => {
  const lessonResult = {
    success: true,
    data: [
      { id: 1, title: '深入理解ES6' },
      { id: 2, title: 'JavaScript高级程序设计' },
      { id: 3, title: 'CSS揭秘' },
      { id: 4, title: '深入浅出Vue.js' }
    ]
  }
  
  it('mock http modules', async () => {
    expect.assertions(1)
    const result = await getLessonList()
    expect(result).toEqual(lessonResult)
  })
  it('render mock http module result', async () => {
    const mockAxiosResult = {
      status: 200,
      data: lessonResult
    }
    getLessonList.mockResolvedValueOnce(mockAxiosResult)
    const wrapper = shallowMount(HelloWorld)
    await wrapper.vm.$nextTick()
    const lessonItems = wrapper.findAll('.lesson-item')
    const lessonList = lessonResult.data
    for (let i = 0; i < lessonItems.length; i++) {
      const item = lessonItems.at(i)
      expect(item.text()).toBe(lessonList[i].title)
    }
  })
})

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

В нашем тестовом примере выше мы попытались предоставить компонент во время монтирования.propsDataилиmocks, мы также можем смонтировать несколько других распространенных опций ниже.

  • data: предоставляется на этапе монтированияdataСвойства в будут объединены и перезаписаны текущим компонентом.data, следующее:
  • slots: если смонтированный компонент имеет содержимое слота, его можно указать вручную на этапе монтажа.slots.
  • stubs: если смонтированный компонент имеет подкомпоненты, вы можете вручную предоставить заглушки для подкомпонентов на этапе монтирования, например:stubs: ['my-child', 'transition', 'router-view', 'router-link']Ждать.
  • localVue: предоставить местныйVueэкземпляр, чтобы предотвратить загрязнение глобальногоVue, который использует сторонний плагин:Vue-Router,Vuexиelement-uiи т. д. очень применимы.

Данные монтирования

Предположим, у нас есть следующие компоненты:

<template>
  <div></div>
</template>

<script>
export default {
  data () {
    return {
      bar: 'bar',
      foo: 'foo'
    }
  }
}
</script>

Теперь мы монтируем тестовый примерdata, чтобы протестировать компонент:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('mount data', () => {
    const wrapper = shallowMount(HelloWorld, {
      data () {
        return {
          foo: 'foo override',
          baz: 'baz'
        }
      }
    })
    expect(wrapper.vm.bar).toBe('bar')
    expect(wrapper.vm.foo).toBe('foo override')
    expect(wrapper.vm.baz).toBe('baz')
  })
})

Слоты маунтов

Предположим, у нас есть следующие компоненты:

<template>
  <div>
    <div class="header-slot">
      <slot name="header" />
    </div>
    <div class="default-slot">
      <slot />
    </div>
     <div class="footer-slot">
      <slot name="footer" />
    </div>
  </div>
</template>

<script>
export default {}
</script>

Затем монтируем соответствующий слот и пишем следующий тестовый пример:

import { mount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('mount data', () => {
    const headerSlot = {
      template: `<div>header slot</div>`
    }
    const defaultSlot = {
      template: `<div>default slot</div>`
    }
    const footerSlot = {
      template: `<div>footer slot</div>`
    }
    const wrapper = mount(HelloWorld, {
      slots: {
        default: defaultSlot,
        header: headerSlot,
        footer: footerSlot
      }
    })
    expect(wrapper.find('.header-slot').html()).toContain(headerSlot.template)
    expect(wrapper.find('.default-slot').html()).toContain(defaultSlot.template)
    expect(wrapper.find('.footer-slot').html()).toContain(footerSlot.template)
  })
})

Гора Стабс

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

Если компонент используетrouter-linkилиrouter-view, но мы не хотим устанавливать его в тестовом примереVue-Router, то его можно заглушить следующим образом:

const wrapper = shallowMount(HelloWorld, {
  stubs: ['router-link', 'router-view']
})

Монтировать сторонние приложения

Иногда мы развиваемсяVueПри подаче заявки часто используются сторонние плагины, такие как:Vue-Router,Vuexа такжеelement-uiи Т. Д. Итак, как изящно установить эти сторонние приложения в тестовом случае?

можно использоватьcreateLocalVue()метод создания локальногоVueНапример, чтобы заменить глобальныйVue, а затем передать этот локальный файл при монтировании компонентаVue,следующее:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import Vuex from 'vuex'
import Router from 'vue-router'
import ElementUI from 'element-ui'
const localVue  = createLocalVue()
localVue.use(Vuex)
localVue.use(Router)
localVue.use(ElementUI)

describe('HelloWorld.vue', () => {
  it('use localVue', () => {
    const wrapper = shallowMount(HelloWorld, {
      localVue
    })
  })
})

изменить состояние компонента

После установки компонента вы можете использовать следующиеsetспособ изменения данных в компоненте.

  • setChecked:настраиватьcheckboxилиradioэлементальcheckedзначение и обновлениеv-model.
  • setSelected: установитьoptionэлемент и обновлениеv-model.
  • setValue: установить текстовый элемент управления илиselectзначение элемента и обновлениеv-model.
  • setProps: установить оберткуvmпримерpropssи обновить.
  • setData: установить в оберткеvmв случаеdataи обновить.

Уведомление:так какVueобновление компонентаDOMявляется асинхронным, поэтому, если мы хотим проверить, что компонент изменяет данныеDOM, мы должны использовать$nextTick().

Предположим, у нас есть следующие компоненты:

<template>
  <div>
    <input v-model="radio" type="radio" :value="true" />
    <select v-model="select">
      <option :value="1">选项一</option>
      <option :value="2">选项而</option>
    </select>
    <input v-model="txt" type="text">
  </div>
</template>

<script>
export default {
  props: {
    msg: String
  },
  data () {
    return {
      foo: '',
      radio: false,
      select: '',
      txt: ''
    }
  }
}
</script>

Затем мы можем написать следующий тестовый пример для проверки компонента после его монтирования:

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

describe('HelloWorld.vue', () => {
  it('change component data', () => {
    const wrapper = shallowMount(HelloWorld)
    const radioInput = wrapper.find('input[type="radio"]')
    const options = wrapper.find('select').findAll('option')
    const textInput = wrapper.find('input[type="text"]')
    radioInput.setChecked()
    options.at(1).setSelected()
    textInput.setValue('txt value')
    expect(wrapper.vm.radio).toBe(true)
    expect(wrapper.vm.select).toBe(2)
    expect(wrapper.vm.txt).toBe('txt value')

    wrapper.setProps({
      msg: 'msg value'
    })
    expect(wrapper.vm.msg).toBe('msg value')

    wrapper.setData({
      foo: 'foo value'
    })
    expect(wrapper.vm.foo).toBe('foo value')
  })
})

тестовое событие

существуетVueВ приложении мы в основном столкнемся с двумя типами событий:Собственные события DOMиПользовательские события Vue

Собственные события DOM

Обычно роднойDOMСобытия в основном используются в качестве входных данных для модульного тестирования.DOMСобытия: нажмите на элемент, чтобы вызватьclickСобытия, при наведении курсора на элемент будут срабатыватьmouseenterСобытие, нажатие любой клавиши на клавиатуре вызоветkeyup/keydownсобытие и отправка формы сработаетsubmitсобытия и т.д.

существуетVue-Test-Utils, каждая оболочка имеетtriggerМетод отправки синтетического события элементу-оболочке. так называемыйсинтетическое событиезначит вJavaScriptСобытие, созданное в , обрабатывается так же, как браузер отправляет событие, разница в том, что нативное событие передается черезEventLoopОбработчики событий вызываются асинхронно, тогда как синтетические события вызываются синхронно.

triggerметоды могут быть использованы для имитации практически любого природногоDOMтакие события, какclick,keydownилиmouseenterЖдать. Предположим, у нас есть такое требование, нажмите на элемент, чтобы реализовать автоинкремент числа:

<template>
  <div>
    {{count}}
    <button @click="count++">自增</button>
  </div>
</template>

<script>
export default {
  props: {
    msg: String
  },
  data () {
    return {
      count: 0
    }
  }
}
</script>

Затем мы можем написать тесты на основе вышеуказанных компонентов.clickМодульные тесты для нативных событий:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('测试原生click事件', async () => {
    const wrapper = shallowMount(HelloWorld)
    const btn = wrapper.find('button')
    expect(wrapper.vm.count).toBe(0)
    btn.trigger('click')
    expect(wrapper.vm.count).toBe(1)
    await wrapper.vm.$nextTick()
    expect(wrapper.text()).toContain(1)
  })
})

Передать параметры события

При запуске события мы также можем передать параметры события,trigger()метод принимает второй необязательныйoptionsпараметры, свойства которых будут установлены в$eventНа объекте:

// 传递事件参数
btn.trigger('click', { count: 10 })

// 获取事件参数
handleIncrementClick ($event) {
  console.log($event.count)
}

Уведомление: мы не можемoptionsУстановить цель события в настройкахtargetзначение, если мыtriggerэто элемент формы, который хочет быть вtriggerПеред изменением значения его формы. С одной стороны, мы можем использовать несколько ранее введенныхsetметод, с другой стороны, мы можем изменитьelementзначение элемента для наших целей:

// setValue改变元素的值
input.setValue(100)
btn.trigger('click')

// element元素改变元素的值
input.element.value = 100
btn.trigger('click')

пользовательское событие

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

  • Для самого компонента, генерирующего событие, генерируемое событие является выходом компонента.
  • Для родительских компонентов, прослушивающих пользовательские события, испускаемые события являются входными данными компонента.

существуетVue-Test-Utils, компонент, который генерирует события, может бытьwrapper.emitted()Get, его возвращаемое значение представляет собой объект, в котором в качестве имени ключа объекта используется имя события, а в качестве значения ключа используется соответствующий параметр, например:

wrapper.vm.$emit('change', 100)
wrapper.vm.$emit('update:visible', false)

const emitted = wrapper.emitted()
console.log(emitted)
/*
{
  'change': [[100]],
  'update:visible': [[false]]  
}
*/

Давайте сначала посмотрим на первый случай, сам компонент запускает событие извне:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('emit触发事件', () => {
    const wrapper = shallowMount(HelloWorld)
    wrapper.vm.$emit('change', 100)
    wrapper.vm.$emit('update:visible', false)

    const emitted = wrapper.emitted()

    expect(emitted['change']).toBeTruthy()
    expect(emitted['change'][0]).toEqual([100])

    expect(emitted['update:visible']).toBeTruthy()
    expect(emitted['update:visible'][0]).toEqual([false])
  })
})

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

<template>
  <div>
    <child-component @custom="onCustom" />
    <p v-if="emitted">Emitted!</p>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent'

  export default {
    name: 'ParentComponent',
    components: { ChildComponent },
    data() {
      return {
        emitted: false
      }
    },
    methods: {
      onCustom() {
        this.emitted = true
      }
    }
  }
</script>

Тогда мы можем написать следующий тестовый пример:

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

describe('ParentComponent', () => {
  it("displays 'Emitted!' when custom event is emitted", () => {
    const wrapper = mount(ParentComponent)
    wrapper.find(ChildComponent).vm.$emit('custom')
    expect(wrapper.html()).toContain('Emitted!')
  })
})

Описание тестового примера:find()Метод может передавать не только известные стандартные селекторы: теги элементов, имена классов и т. д., но и компонент, который возвращает обертку для компонента.

Тест Vuex

вVue应用程序中часто используетсяVuex, у нас есть следующие способы проверитьVuex:

  • тест в одиночкуstoreкаждая часть: мы можем поставитьstoreсерединаmutations,actionsиgettersОтдельно разделить и проверить отдельно.
  • Комбинированный тестstore: мы не расстаемсяstore, а в целом тестируемstoreэкземпляр, и, надеюсь, он будет выводиться, как ожидалось.

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

Тестовые мутации

дляmutation, это просто функция, поэтомуmutationЮнит-тестирование очень простое. Нам просто нужно передать параметр и ожидатьstateможет выводить правильно. предположимmutationкод показывает, как показано ниже:

// mutations.js
setToken (state, token) {
  state.token = token
}

Мы можем написать следующие модульные тесты на основе приведенного выше кода:

import mutations from './mutations.js'
describe('mutations', () => {
  it('test setToken mutations', () => {
    const token = '123456'
    const state = {
      token: ''
    }
    mutations.setToken(state, token)
    expect(state.token).toBe(token)
  })
})

Тестовые геттеры

такой жеmutationsТакой же,gettersТакже нормальная функция, она всегда возвращает значение. Так что это делает тестgettersУпростилось, надо только утверждатьgetterВозвращаемого значения функции достаточно. Предположим, у нас есть следующееgettersКод:

// getters.js
export const passList = state => {
  return state.students.filter(stu => stu.score >= 60)
}

Мы можем написать следующие модульные тесты на основе приведенного выше кода:

import getters from './getters.js'
describe('getters', () => {
  it('test passList getters', () => {
    const students = [
      { name: 'AAA', score: 59 },
      { name: 'BBB', score: 70 },
      { name: 'CCC', score: 10 }
    ]
    const state = {
      students: students
    }
    const result = getters.passList(state)
    expect(result).toEqual(students[1])
  })
})

Тестовые действия

отличается отmutations, моделируем отдельноactionsЧтобы немного усложнить, предположим, что у нас есть следующееactionsКод:

// actions.js
export const login = ({ commit }, { userInfo, token }) {
  commit('setUserInfo', userInfo)
  commit('setToken', token)
}

Мы можем написать следующие модульные тесты на основе приведенного выше кода:

import actions from './actions.js'
describe('actions', () => {
  const loginResult = {
    userInfo: { name: 'AAA', age: 23 },
    token: '123456'
  }
  it('test login action', () => {
    expect.assertions(1)
    const context = {
      commit: jest.fn()
    }
    actions.login(context, loginResult)
    expect(context.commit).toHaveBeenCalledWith('setToken', loginResult.token)
  })
})

Протестируйте экземпляр Vuex Store

Комбинированный тестstoreСледует отметить, что нам нужно использоватьlocalVueа не глобальноеVueсмонтировать нашVuex, предполагая, что у нас есть следующий код:

test('increment updates state.count by 1', () => {
  Vue.use(Vuex)
  const store = new Vuex.store({storeConfig})
  expect(store.state.count).toBe(0)

  store.commit('increment')
  expect(store.state.count).toBe(1)
})

Когда мы запускаем приведенный выше код для отдельных тестов, он выводит, как мы и ожидали, но возникает фатальная проблема: если у нас есть несколько тестовых случаев, потому чтоstoreОбъекты являются ссылочными типами, и значение, которое мы изменяем в первом тестовом примере, повлияет на другие тестовые примеры.

Осуществимый подход, когда мы используем каждый разstoreкогда используешьcloneDeep(store)После копирования это решает проблему ссылок на объекты, ноVue-Test-UtilsОбеспечивает более дружественный способ решения такого рода проблем:localVue.

localVueКак мы упоминали ранее, мы можем преобразовать приведенный выше тестовый пример так же, как описано ранее:

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

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

test('increment updates state.count by 1', () => {
  const store = new Vuex.store({storeConfig})
  expect(store.state.count).toBe(0)

  store.commit('increment')
  expect(store.state.count).toBe(1)
})

Чтобы протестировать полныйstore, мы предполагаем следующий код:

import * as types from './mutation-types.js'
import {
  setToken,
  getToken,
  removeToken,
  setUserInfo,
  getUserInfo,
  removeUserInfo
} from '@/utils/cache.js'
const state = {
  token: getToken(),
  userInfo: getUserInfo()
}
const mutations = {
  [types.SET_TOKEN] (state, token) {
    state.token = token
  },
  [types.SET_USER_INFO] (state, userInfo) {
    state.userInfo = userInfo
  }
}
const actions = {
  login ({ commit }, { token, userInfo }) {
    commit(`${types.SET_TOKEN}`, setToken(token))
    commit(`${types.SET_USER_INFO}`, setUserInfo(userInfo))
  },
  logout ({ commit }) {
    commit(`${types.SET_TOKEN}`, removeToken())
    commit(`${types.SET_USER_INFO}`, removeUserInfo())
  }
}

Мы также определяемgetters.jsкод показывает, как показано ниже:

export const token = (state) => state.token

export const userInfo = (state) => state.userInfo

Таким образом, мы можем написать следующий тест в целомstoreкод:

import store from './store/index.js'
import * as types from './store/mutation-types.js'
import * as getters from './store/getters.js'

describe('test store', () => {
  beforeEach(() => {
    localStorage.clear()
  })
  it('test login action', () => {
    const loginResult = {
      userInfo: { name: 'AAA', age: 23 },
      token: '123456'
    }
    expect(getters.token).toBe('')
    expect(getters.userInfo).toEqual({})

    store.dispatch('login', loginResult)
    expect(getters.token).toBe(loginResult.token)
    expect(getters.userInfo).toEqual(loginResult.userInfo)
  })
  it('test logout action', () => {
    store.dispatch('logout')
    expect(getters.token).toBe('')
    expect(getters.userInfo).toEqual({})
  })
})

Тест Vue-маршрутизатора

когдаVue-Routerустанавливается наVueПозже он добавляет два свойства экземпляра:$routeи$router, после добавления этих двух свойств их нельзя переопределить.

  • $route: Содержит информацию о текущем маршруте, включая любые динамические поля в параметрах маршрута.
  • $router: текущий экземпляр маршрутизации, который содержит все методы, которые могут управлять текущей маршрутизацией, например:push,replaceиbackЖдать.

проверить $маршрут

Когда наш компонент использует$routeатрибут экземпляра, то этот атрибут станет зависимостью компонента. Мы уже представили его ранее. Возможным способом работы с зависимостью является моделирование. Предположим, у нас есть следующие компоненты:

<template>
  <div>
    <p v-if="$route.query && $route.query.id">get detail</p>
    <p v-else>need passed id</p>
  </div>
</template>

Мы видим, что вышеперечисленные компоненты используются$routeinstance, поэтому мы пишем следующий тестовый код для вышеуказанного компонента:

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

describe('HelloWorld.vue', () => {
  let $route 
  beforeEach(() => {
    $route = {
      query: {}
    }
  })
it('no passed $route.query.id', () => {
    const wrapper = shallowMount(HelloWorld, {
      mocks: {
        $route
      }
    })
    expect(wrapper.text()).toContain('need passed id')
  })
  it('passed $route.query.id', () => {
    $route.query.id = 123
    const wrapper = shallowMount(HelloWorld, {
      mocks: {
        $route
      }
    })
    expect(wrapper.text()).toContain('get detail')
  })
})

проверить $маршрутизатор

проверено выше$routeНа основе компонента дополнительно модифицируем код:

<template>
  <div>
    <p v-if="$route.query.id">get detail</p>
    <p v-else>need passed id</p>
  </div>
</template>
<script>
export default {
  mounted () {
    if (!this.$route.query.id) {
      this.$router.replace('/home')
    }
  }
}
</script>

затем мы тестируем$routerКод можно написать так:

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

describe('HelloWorld.vue', () => {
  let $route
  let $router
  beforeEach(() => {
    $route = {
      query: {}
    }
    $router = {
      replace: jest.fn()
    }
  })
  it('replace home when no id', () => {
    const wrapper = shallowMount(HelloWorld, {
      mocks: {
        $route,
        $router
      }
    })
    expect($router.replace).toHaveBeenCalled()
  })
})

Тестовый маршрутизаторСсылка

Это все еще вышеуказанный компонентный код в качестве примера, мы добавилиrouter-link, когда не прошелidпараметр, мы позволяем пользователю вручную щелкнуть, чтобы вернуться:

<template>
  <div>
    <p v-if="$route.query.id">get detail</p>
    <p v-else>
      need passed id
      <router-link to="/home">返回</router-link>
    </p>
  </div>
</template>

Все мы знаем, что после установкиVue-RouterПосле этого мы можем использоватьrouter-linkиrouter-viewи другие встроенные компоненты, но если мы не делаем другую обработку, мы не можем поставитьrouter-linkкак компонент, а затем в соответствии сwrapper.findComponent()способ найти его.

существуетVue-Test-Utils, мы можем использоватьstudsзаглушкаrouter-link, затем используйтеRouterLinkStubконтрольrouter-linkОказывать:

import { shallowMount, RouterLinkStub } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
const wrapper = shallowMount(HelloWorld, {
  stubs: {
    RouterLink: RouterLinkStub
  }
})

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

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

describe('HelloWorld.vue', () => {
  it('render router-link', () => {
    const $route = {
      query: {}
    }
    const wrapper = shallowMount(HelloWorld, {
      stubs: {
        RouterLink: RouterLinkStub
      },
      mocks: {
        $route
      }
    })
    expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/home')
  })
})

Уведомление: рекомендуется, если мы ищем теги элементовwrapper.find()метод, если вы ищете компоненты, рекомендуется использоватьwrapper.findComponent().

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

Протестируйте миксин

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

Предположим, у нас есть следующееtitleMixinКод:

/// mixin.js
export const titleMixin = {
  mounted () {
    const title = this.title
    if (title) {
      document.title = title
    }
  }
}

Затем мы используем это в компонентеmixin:

<template>
  <div>
    Hello,Vue.js
  </div>
</template>
<script>
import { titleMixin } from '@/mixin/index.js'
export default {
  mixins: [titleMixin],
  data () {
    return {
      title: '测试title mixin'
    }
  }
}
</script>

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

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('test mixin', () => {
    const wrapper = shallowMount(HelloWorld)
    expect(document.title).toBe('测试title mixin')
  })
})

тестовые фильтры

контрольная работаfiltersтак жеmixinsОчень похоже, предположим, что у нас есть следующая перевернутая строкаfilterКод:

export const reverseStr = (str) => {
  if (!str) {
    return
  }
  if (typeof str !== 'string') {
    return str
  }
  return str.split('').reverse().join('')
}

Далее мы используем это в компонентеfilter:

<template>
  <div>
    <span>{{msg | reverseStr}}</span>
    <span>{{age | reverseStr}}</span>
  </div>
</template>
<script>
import { reverseStr } from '@/filters/index.js'
export default {
  filters: {
    reverseStr
  },
  data () {
    return {
      age: 23,
      msg: 'ABC',
    }
  }
}
</script>

Наконец, мы пишем тестовые примеры для вышеуказанных компонентов:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
  it('test filters', () => {
    const wrapper = shallowMount(HelloWorld)
    expect(wrapper.text()).toContain('CBA')
    expect(wrapper.text()).toContain('23')
  })
})

снимок теста

Простое объяснение тестирования моментальных снимков состоит в том, чтобы сделать снимок кода и сравнить его с ранее сохраненным снимком. Если новый снимок не соответствует предыдущему снимку, тест завершится неудачно. Снэпшот-тесты относительно полезны для тестирования компонента, потому что добавление снэпшот-теста предотвращает ошибочное изменение компонента.

существуетJestВ рамках автоматизированного тестирования мы можем использовать следующий код для создания моментального снимка компонента:

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

it('match snapshot', () => {
  const wrapper = shallowMount(HelloWorld)
  expect(wrapper.element).toMatchSnapshot()
})

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

  1. Запустите тест моментального снимка.
  2. Генерация выходных данных.
  3. Существовал ли снимок ранее.
  4. не существует, создайте снимок, тест пройден
  5. Если он существует, продолжайте сравнивать его с предыдущим снимком, чтобы убедиться, что он такой же.
  6. То же, тест проходит.
  7. Не то же самое, тест не проходит.

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

exports[`HelloWorld.vue match snapshot 1`] = `
<div>
  <span
    class="item"
  >
    item
  </span>
  Hello, Vue and Jest...
</div>
`;

Моментальные снимки статических компонентов

статические компоненты: относится к компоненту, который всегда выводит один и тот же вывод, он не принимает никакихprop, ни какой-либоstate, внутри компонента также нет никакой логики, и он всегда будет отображать одно и то же.HTMLэлемент. Писать юнит-тесты для статических компонентов совершенно необязательно, но для компонента нужно написать снэпшот-тест.

Предположим, у нас есть следующий статический компонент:

<template>
  <transition>
    <svg class="spinner" width="44px" height="44px" viewBox="0 0 44 44">
      <circle class="path" fill="none" stroke-width="4" stroke-linecap="round" cx="22" cy="22" r="20">
    </svg>
  </transition>
</template>

Мы пишем следующие тестовые примеры статических снимков:

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

describe('spinner.vue', () => {
  it('match snapshot', () => {
    const wrapper = shallowMount(Spinner)
    expect(wrapper.element).toMatchSnapshot()
  })
})

Моментальные снимки динамических компонентов

динамические компоненты: Относится к тем компонентам, которые содержат логику и состояние, например нажатие кнопкиpropsзначение или изменить данные компонента. При написании тестовых случаев для динамических компонентов вы должны попытаться захватить наиболее важные ветви логики. Поскольку для крупного компонента на него будут влиятьprops,самdataИли другие данные влияют на результат рендеринга компонента, и мы не можем написать снэпшот-тест для каждой логики ветки.

Предположим, у нас есть следующий код компонента:

<template>
  <div>
    <div v-if="age < 10"> child person </div>
    <div v-else-if="age>=10&&age<30">youth person</div>
    <div>{{msg}}</div>
  </div>
</template>
<script>
export default {
  props: {
    msg: {
      type: String,
      default: 'default msg'
    }
  },
  data () {
    return {
      age: 11
    }
  }
}
</script>

Мы пишем следующие два тестовых примера моментального снимка на основе приведенного выше кода:

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

describe('HelloWorld.vue', () => {
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(HelloWorld)
  })
  it('match msg snapshot', () => {
    expect(wrapper.element).toMatchSnapshot()
  })
  it('match age snapshot', async () => {
    wrapper.setData({
      age: 6
    })
    await wrapper.vm.$nextTick()
    expect(wrapper.element).toMatchSnapshot()
  })
})

бегатьnpm run test:unit, мы получим такой снимок:

exports[`HelloWorld.vue match age snapshot 1`] = `
<div>
  <div>
     child person 
  </div>
   
  <div>
    default msg
  </div>
</div>
`;

exports[`HelloWorld.vue match msg snapshot 1`] = `
<div>
  <div>
    youth person
  </div>
   
  <div>
    default msg
  </div>
</div>
`;

обновить снимок

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

Есть две основные ситуации для обновления моментальных снимков:

  • Обновить все: мы можем использоватьnpm run test:unit -- -uКоманда для пакетного обновления наших снапшотов, но этот метод очень опасен, потому что очень вероятно, что один из компонентов является незапланированным обновлением, и при пакетном обновлении снапшота будет сгенерирован неверный файл снапшота.
  • Интерактивное обновление: мы можем использоватьnpm run test:unit -- --watchкоманда продолжитьJestинтерактивное обновление,iключ для просмотра всех неудачных файлов моментальных снимков,uКлюч обновляет ранее сохраненный снимок последним значением. Другие кнопки см.Jestконтент на официальном сайте.
Категории