2

问题

我正在使用带有 Mocha 和 Chai 的 Vue 2.4 进行单元测试。在执行组件测试时,每个测试的执行时间都比之前的测试长。

期待

即每次调用shallow/shallowMount耗时不超过 200ms。

文件

package.json

{
  "name": "client",
  "version": "1.0.0",
  "description": "client",
  "private": true,
  "scripts": {
    "dev": "node build/dev-server.js",
    "start": "node ./server.js",
    "build": "node build/build.js",
    "test": "mocha-webpack --webpack-config build/webpack.dev.conf.js --require test/setup.js src/**/*.spec.js src/*.spec.js --timeout 10000",
    "test:watch": "mocha-webpack --webpack-config build/webpack.dev.conf.js --require test/setup.js src/**/*.spec.js src/*.spec.js --watch --timeout 10000",
    "test:e2e": "node test/e2e/runner.js",
    "heroku-postbuild": "yarn build"
  },
  "standard": {
    "parser": "babel-eslint",
    "plugins": [
      "html"
    ],
    "ignore": [
      "dist"
    ]
  },
  "engines": {
    "node": "8.2.1",
    "npm": ">= 3.0.0"
  },
  "dependencies": {
    "auth0-js": "^9.5.1",
    "axios": "^0.16.2",
    "babel-polyfill": "^6.26.0",
    "execa": "^0.8.0",
    "express-force-ssl": "^0.3.2",
    "node-sass": "^4.7.2",
    "onsenui": "^2.6.1",
    "query-string": "^5.0.0",
    "ramda": "^0.25.0",
    "sass-loader": "^6.0.6",
    "validator": "^9.4.1",
    "vue": "^2.4.0",
    "vue-browserupdate": "^1.2.0",
    "vue-i18n": "^7.3.0",
    "vue-mq": "^0.1.0",
    "vue-onsenui": "^2.2.1",
    "vue-stash": "^2.0.1-beta",
    "vue-touch": "next",
    "vuex": "^2.4.1"
  },
  "devDependencies": {
    "@vue/test-utils": "^1.0.0-beta.14",
    "autoprefixer": "^7.1.4",
    "babel-core": "^6.22.1",
    "babel-eslint": "7.2.3",
    "babel-loader": "^7.1.2",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.26.0",
    "chai": "^4.1.2",
    "chai-spies": "^1.0.0",
    "chalk": "^2.1.0",
    "chromedriver": "^2.27.2",
    "compression-webpack-plugin": "^1.0.1",
    "connect-history-api-fallback": "^1.3.0",
    "copy-webpack-plugin": "^4.0.1",
    "cross-env": "^5.0.5",
    "cross-spawn": "^5.0.1",
    "css-loader": "^0.28.0",
    "eslint-plugin-html": "^3.2.0",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.16.1",
    "extract-text-webpack-plugin": "^3.0.1",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.1.3",
    "html-webpack-plugin": "^2.28.0",
    "http-proxy-middleware": "^0.17.3",
    "jsdom": "^11.8.0",
    "jsdom-global": "^3.0.2",
    "mocha": "^5.1.1",
    "mocha-webpack": "^1.1.0",
    "nightwatch": "^0.9.12",
    "opn": "^5.1.0",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "rimraf": "^2.6.0",
    "selenium-server": "^3.0.1",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "standard": "^11.0.0",
    "sw-precache-webpack-plugin": "^0.11.4",
    "url-loader": "^0.5.8",
    "vue-loader": "^13.0.5",
    "vue-style-loader": "^4.0.2",
    "vue-template-compiler": "^2.4.0",
    "vueify": "^9.4.1",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^2.2.1",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.18.0",
    "webpack-merge": "^4.1.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}

为了尽可能轻松地设置组件测试,我们使用了一个夹具类 ( VueComponentTestFixture):

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Vuex from 'vuex'
import { shallow, mount, createLocalVue } from '@vue/test-utils'

import chai from 'chai'
import spies from 'chai-spies'
chai.use(spies)

import messages from '../../src/locale/index'

Vue.use(VueI18n)
const i18n = new VueI18n({ locale: 'de', messages, silentTranslationWarn: true })
Vue.use(Vuex)

// Prevent warnings during tests
// https://github.com/vuetifyjs/vuetify/issues/243#issuecomment-288467099
Vue.config.ignoredElements = [
  'v-ons-toolbar',
  'v-ons-toolbar-button',
  'v-ons-search-input',
  'v-touch',
  'v-ons-dialog',
  'v-ons-list',
  'v-ons-list-item',
  'v-ons-page',
  'v-ons-list-header',
  'v-ons-icon',
  'v-ons-modal',
  'ons-list-item',
  'v-ons-select',
  'v-ons-input',
  'v-ons-radio',
  'ons-alert-dialog-button',
  'ons-list',
  'v-ons-button',
  'v-ons-checkbox',
]

const localVue = createLocalVue()

// Prevent a lot of "[vuex] unknown mutation type .." logs during tests
const originalConsoleError = console.error
console.error = function(msg) {
  if(msg.startsWith('[vuex] unknown')) return
  originalConsoleError(msg)
}

export default class VueComponentTestFixture {
  state = {}
  actions = {}
  getters = {}
  mutations = {}
  props = {}
  onsen = {
    notification: {
      alert: chai.spy(),
      confirm: chai.spy(),
      toast: chai.spy()
    }
  }
  mq = {}

  constructor(componentInstance) {
    this.component = componentInstance
  }

  buildWrapper() {
    const startTime = Date.now()
    // custom plugin mocks
    Vue.prototype.$mq = this.mq
    Vue.prototype.$ons = this.onsen

    // build test store
    let store = new Vuex.Store({
      state: this.state,
      actions: this.actions,
      getters: this.getters,
      mutations: this.mutations
    })

    // build component wrapper using shallow rendering
    let wrapper = shallow(this.component, { store, localVue, i18n, propsData: this.props })
    console.log(`buildWrapper mount: ${(Date.now() - startTime)}ms`)
    return wrapper
  }
}

测试文件示例:

import VueComponentTestFixture from '../../test/components/VueComponentTestFixture'
import chai from 'chai'
import spies from 'chai-spies'
chai.use(spies)
const expect = chai.expect

import Input from './Input.vue'

describe('Input.vue spy', () => {
  let fixture
  let wrapper

  beforeEach(() => {
    fixture = new VueComponentTestFixture(Input)
    wrapper = fixture.buildWrapper()
  })

  it('correctly emits NUMBER type values', () => {
    // Arrange
    wrapper.setProps({
      isEditable: true,
      value: undefined,
      number: undefined,
      type: 'NUMBER',
      title: 'test'
    })
    const input = wrapper.find('[data-test-id="number-input"]')
    const value = 100

    // Act
    input.element.value = value
    input.trigger('input')
    wrapper.find('[data-test-id="save-button"]').trigger('click')

    // Assert
    expect(wrapper.emitted().change[0][0]).to.equal(value)
  })

  it('correctly emits TEXT type values', () => {
    // Arrange
    wrapper.setProps({
      isEditable: true,
      value: undefined,
      number: undefined,
      type: 'TEXT',
      title: 'test'
    })
    const input = wrapper.find('[data-test-id="text-input"]')
    const value = 'test'

    // Act
    input.element.value = value
    input.trigger('input')
    wrapper.find('[data-test-id="save-button"]').trigger('click')

    // Assert
    expect(wrapper.emitted().change[0][0]).to.equal(value)
  })

  it("doesn't emit when not editable", () => {
    // Arrange
    wrapper.setProps({
      isEditable: false,
      value: undefined,
      number: undefined,
      type: 'NUMBER',
      title: 'test'
    })
    const input = wrapper.find('[data-test-id="number-input"]')
    const value = 100

    // Act
    input.element.value = value
    input.trigger('input')
    wrapper.find('[data-test-id="save-button"]').trigger('click')

    // Assert
    expect(wrapper.emitted()).to.be.empty
    expect(input.attributes().disabled).to.equal('disabled')
  })
})

这是执行测试时的输出(夹具中的时间测量):

  EventEditor.vue basics
    ✓ name present
    ✓ data present

  EventEditor.vue spy
    modulePath module
buildWrapper mount: 110ms
buildWrapper mount: 89ms
      ✓ dispatches "createEvent" when clicking on an enabled eventType button (210ms)
buildWrapper mount: 164ms
      ✓ doesn't dispatch "createEvent" when clicking on a disabled eventType button (161ms)
buildWrapper mount: 230ms
buildWrapper mount: 262ms
      ✓ emits "change" correctly when event is added (515ms)
buildWrapper mount: 310ms
buildWrapper mount: 365ms
      ✓ needs a confirmation for removing an event (720ms)
buildWrapper mount: 409ms
buildWrapper mount: 457ms
      ✓ shows finish timeline button correctly (460ms)
buildWrapper mount: 515ms
      ✓ emits "request-close-timeline" correctly when finish timeline button is clicked (504ms)
    modulePath events
buildWrapper mount: 574ms
buildWrapper mount: 682ms
      ✓ dispatches "createEvent" when clicking on an enabled eventType button (1325ms)
buildWrapper mount: 682ms
      ✓ doesn't dispatch "createEvent" when clicking on a disabled eventType button (678ms)
buildWrapper mount: 728ms
buildWrapper mount: 775ms
      ✓ emits "change" correctly when event is added (1546ms)
buildWrapper mount: 843ms
buildWrapper mount: 899ms
      ✓ needs a confirmation for removing an event (1846ms)
buildWrapper mount: 954ms
buildWrapper mount: 1081ms
      ✓ shows finish timeline button correctly (1082ms)
buildWrapper mount: 1220ms
      ✓ emits "request-close-timeline" correctly when finish timeline button is clicked (1245ms)

  Home component
    ✓ name present
    ✓ components present

  Input.vue spy
buildWrapper mount: 1137ms
    ✓ correctly emits NUMBER type values (2215ms)
buildWrapper mount: 1181ms
    ✓ correctly emits TEXT type values (2215ms)
buildWrapper mount: 1177ms
    ✓ doesn't emits when not editable

  ObservationDialog.vue spy
buildWrapper mount: 1236ms
    ✓ sets locations correctly
buildWrapper mount: 1278ms
    ✓ enables continue buttons only if valid data is given
buildWrapper mount: 1345ms
    ✓ dispatches action "createTimeline"
buildWrapper mount: 1404ms
    ✓ resets form values when createTimeline is called
buildWrapper mount: 1443ms
    ✓ contains all eventTypes in "event selection" when no module is selected
buildWrapper mount: 1467ms
    ✓ removes eventTypes from "event selection" when a module is selected
    method createTimeline
buildWrapper mount: 1547ms
buildWrapper mount: 1581ms
      ✓ dispatches no action when no variant is selected
buildWrapper mount: 1616ms
buildWrapper mount: 1675ms
      ✓ dispatches "createTimeline" when module variant is selected
buildWrapper mount: 1758ms
buildWrapper mount: 1836ms
      ✓ dispatches "loadObservationEvents" when event variant is selected
buildWrapper mount: 1873ms
buildWrapper mount: 2133ms
      ✓ dispatches "createTimeline" and "loadObservationEvents" when both variants are selected
buildWrapper mount: 2953ms
buildWrapper mount: 2445ms
      ✓ commits no mutations when no variant is selected
buildWrapper mount: 2349ms
buildWrapper mount: 2099ms
      ✓ commits the expected mutations when module variant is selected
buildWrapper mount: 2835ms
buildWrapper mount: 2654ms
      ✓ commits the expected mutations when event variant is selected
buildWrapper mount: 3052ms
buildWrapper mount: 2722ms
      ✓ commits only resetPageStack when module and events observation is selected
buildWrapper mount: 3129ms
buildWrapper mount: 2988ms
      ✓ commits the expected mutations when both variants are selected

  ObservationMenu.vue basics
    ✓ name present
    ✓ data present
    ✓ computed present

  ObservationMenu.vue spy
buildWrapper mount: 3490ms
    ✓ sets timelines correctly
buildWrapper mount: 3225ms
    ✓ opens dialog correctly (2440ms)
buildWrapper mount: 2658ms
    ✓ triggers activeModule mutation on state when open existing new timeline (2768ms)
buildWrapper mount: 4084ms
    ✓ triggers activeModule mutation on state when open existing finished timeline (3000ms)

  Select.vue
    ✓ name present
    ✓ props present
    ✓ data present

  Select.vue spy
buildWrapper mount: 2724ms
    ✓ shows only prefered options
buildWrapper mount: 2714ms
    ✓ opens modal (2925ms)

  state
    ✓ commit reset action
    ✓ empties the page stack
    mutation navigateToTimeline
      ✓ adds a "module observation" page to the beginning of the pageStack array
      ✓ adds an "event observation" page to the end of the pageStack array


  50 passing (2m)

可以看出buildWrapper每次测试“叠加”所消耗的时间。我用beta-14of vue-test-utilsas 也试过了beta-20,结果相同。

我认为组件本身并不重要。当仅单独执行每个组件的测试时也会发生这种情况。但如果有必要,请告诉我。

4

1 回答 1

0

我不确定这是否能解决问题,但这里有一些我一直在做的事情:

  1. 将“非破坏性”测试(即仅确定元素存在或不存在的测试,没有任何点击或数据填充,检查快照有效性等)组合成一个子描述,例如
describe('main', () => {
    describe('non-destructive', () => { ... });
    describe('destructive', () => { ... });
});

每个描述都可以有自己的一组 beforeEach/afterEach、beforeAll/afterAll 钩子。

  1. 在非破坏性测试的情况下,使用beforeAll代替beforeEach,因为安装只需要发生一次

  2. 对于破坏性测试,添加afterEach()将调用wrapper.destroy()以确保测试自行清理的钩子:

 afterEach(() => {
    wrapper.destroy();
  });

您可以在此处添加任何其他清理,例如,如果您正在模拟 axios,那么在mockedAxios.resetHistory();这里执行是个好主意

于 2019-11-08T01:03:41.023 回答