Vue cli3 与 karma

Mocha

Mocha是JavaScript的测试框架, 浏览器和Node端都可以使用。可是Mocha自己并不提供断言的功能, 须要借助例如: Chai, 这样的断言库完成测试的功能。html

Mocha API 速览

Mocha的API快速浏览, 更多细节请参考文档vue

⚠️ 注意:node

  1. Mocha不推荐使用箭头函数做为Callback的函数
  2. 不要用例中什么都不作, 推荐使用skip跳过不须要的测试

Mocha简单的示例

describe('unit', function () {
  it('example', function () {
    return true
  })
})
复制代码

Mocha测试异步代码

Mocha支持Promise, Async, callback的形式webpack

// callback
describe('异步测试Callback', function () {
  it('Done用例', function (done) {
    setTimeout(() => {
      done()
    }, 1000)
  })
})

// promise
describe('异步测试Promise', function () {
  it('Promise用例', function () {
    return new Promise((resolve, reject) => {
      resolve(true)
    })
  })
})

// async
describe('异步测试Async', function () {
  it('Async用例', async function () {
    return await Promise.resolve()
  })
})
复制代码

钩子

  1. before, 所有的测试用例以前执行
  2. after, 所有的测试用例结束后执行
  3. beforeEach, 每个测试用例前执行
  4. afterEach, 每个测试用例后执行
// before, beforeEach, 1, afterEach, beforeEach, 2, afterEach, after
describe('MochaHook', function () {
  before(function () {
    console.log('before')
  })

  after(function () {
    console.log('after')
  })

  beforeEach(function () {
    console.log('beforeEach')
  })

  afterEach(function () {
    console.log('afterEach')
  })

  it('example1', function () {
    console.log(1)
  })

  it('example2', function () {
    console.log(2)
  })
})
复制代码

异步钩子

Mocha Hook能够是异步的函数, 支持done,promise, asyncgit

全局钩子(root hook)

若是beforeEach在任何descride以外添加, 那么这个beforeEach将被视为root hook。beforeEach将会在任何文件, 任何的测试用例前执行。github

beforeEach(function () {
  console.log('root beforeEach')
})

describe('unit1', function () {
  //...
})
复制代码

DELAYED ROOT SUITE

若是须要在任何测试用例前执行异步操做也能够使用(DELAYED ROOT SUITE)。使用"mocha --delay"执行测试脚本。"mocha --delay"会添加一个特殊的函数run()到全局的上下文。当异步操做完成后, 执行run函数能够开始执行测试用例web

function deplay() {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve()
    }, 1000)
  })
}

deplay().then(function () {
  // 异步操做完成后, 开始执行测试
  run()
})

describe('unit', function () {
  it('example', function () {
    return true
  })
})
复制代码

skip

describe, 或者it以后添加skip。可让Mocha忽略测试单元或者测试用例。使用skip, 测试会标记为待处理。vue-router

重试测试

设置测试失败后, 测试重试的次数chrome

describe('retries', function () {
  it('retries', function () {
    // 设置测试的重试次数
    this.retries(3)
    const number = Math.random()
    if (number > 0.5) throw new Error()
    else return true
  })
})
复制代码

动态生成测试

对于一些接口的测试, 能够使用动态生产测试用例, 配置请求参数的数组, 循环数组动态生成测试用例vue-cli

describe('动态生成测试用例', function () {
  let result = []
  for(let i = 0; i < 10; i ++) {
    result.push(Math.random())
  }
  result.forEach((r, i) => {
    // 动态生成测试用例
    it(`测试用例${i + 1}`, function () {
      return r < 1
    })
  })
})

复制代码

slow

若是测试用例, 运行时间超过了slow设置的时间, 会被标红。

describe('unit', function () {
  it('example', function (done) {
    this.slow(100)
    setTimeout(() => {
      done()
    }, 200)
  })
})
复制代码

timeout

设置测试用例的最大超时时间, 若是执行时间超过了最大超时时间,测试结果将为错误

describe('unit', function () {
  it('example', function (done) {
    this.timeout(100)
    setTimeout(() => {
      done()
    }, 200)
  })
})
复制代码

Chai

Chai是Node和浏览器的BDD/TDD断言库。下面将介绍, BDD风格的API, expect。should兼容性较差。

Chai API 速览

Chai的API快速浏览, 更多细节请参考文档

not

对断言结果取反

it('not', function () {
  const foo = 1
  expect(foo).to.equal(1)
  expect(foo).to.not.equal(2)
})
复制代码

deep

进行断言比较的时候, 将进行深比较

it('deep', function () {
  const foo = [1, 2, 3]
  const bar = [1, 2, 3]
  expect(foo).to.deep.equal(bar)
  expect(foo).to.not.equal(bar)
})
复制代码

nested

启用点和括号表示法

it('nested', function () {
  const foo = {
    a: [
      {
        a: 1
      }
    ]
  }
  expect(foo.a[0].a).to.equals(1)
})
复制代码

own

断言时将会忽略对象prototype上的属性

it('own', function () {
  Object.prototype.own = 1
  const a = {}
  expect(a).to.not.have.own.property('own')
  expect(a).to.have.property('own')
})
复制代码

ordered

ordered.members比较数组的顺序是否一致, 使用include.ordered.members能够进行部分比较, 配合deep则能够进行深比较

it('order', function () {
  const foo = [1, 2, 3]
  const bar = [1, 2, 3]
  const faz = [1, 2]
  const baz = [{a: 1}, {b: 2}]
  const fzo = [{a: 1}, {b: 2}]
  expect(baz).to.have.deep.ordered.members(fzo)
  expect(foo).to.have.ordered.members(bar)
  expect(foo).to.have.include.ordered.members(faz)
})
复制代码

any

要求对象至少包含一个给定的属性

it('any', function () {
  const foo = {a: 1, b: 2}
  expect(foo).to.have.any.keys('a', 'b', 'c')
})
复制代码

all

要求对象包含所有给定的属性

it('all', function () {
  const foo = {a: 1, b: 2}
  expect(foo).to.not.have.all.keys('a', 'c')
})
复制代码

a, an

用来断言数据类型。推荐在进行更多的断言操做前, 首先进行类型的判断

it('a', function () {
  expect('123').to.a('string')
  expect(false).to.a('boolean')
  expect({a: 1}).to.a('object')
  expect('123').to.an('string')
  expect(false).to.an('boolean')
  expect({a: 1}).to.an('object')
})
复制代码

include

include断言是否包含, include能够字符串, 数组以及对象(key: value的形式)。同时能够配合deep进行深度比较。

it('include', function () {
  expect('love fangfang').to.an('string').to.have.include('fangfang')
  expect(['foo', 'bar']).to.an('array').to.have.include('foo', 'bar')
  expect({a: 1, b: 2, c: 3}).to.an('object').to.have.include({a: 1})
  expect({a: {b: 1}}).to.an('object').to.deep.have.include({a: {b: 1}})
  expect({a: {b: [1, 2]}}).to.an('object').to.nested.deep.have.include({'a.b[0]': 1})
})
复制代码

ok, true, false, null, undefined, NaN

  1. ok断言, 相似"=="
  2. true断言, 相似"==="
  3. flase断言, 与false进行"==="比较
  4. null断言, 与null进行"==="比较
  5. undefined断言, 与undefined进行"==="比较
  6. NaN断言, 与NaN进行"==="比较

empty

断言数组, 字符串长度为空。或者对象的可枚举的属性数组长度为0

equal

进行"==="比较的断言

eql

能够不使用deep进行严格类型的比较

above, least, below, most

  1. above大于断言
  2. least大于等于断言
  3. below小于断言
  4. most小于等于断言

within

范围断言

it('within', function () {
  expect(2).to.within(1, 4)
})
复制代码

property

断言目标是否包含指定的属性

it('property', function () {
  expect({a: 1}).to.have.property('a')
  // property能够同时对key, value断言
  expect({a: { b: 1 }}).to.deep.have.property('a', {b: 1})
  expect({a: 1}).to.have.property('a').to.an('number')
})
复制代码

lengthOf

断言数组, 字符串的长度

it('lengthOf', function () {
  expect([1, 2, 3]).to.lengthOf(3)
  expect('test').to.lengthOf(4)
})
复制代码

keys

断言目标是否具备指定的key

it('keys', function () {
  expect({a: 1}).to.have.any.keys('a', 'b')
  expect({a: 1, b: 2}).to.have.any.keys({a: 1})
})
复制代码

respondTo

断言目标是否具备指定的方法

it('respondTo', function () {
  class Foo {
    getName () {
      return 'fangfang'
    }
  }
  expect(new Foo()).to.have.respondsTo('getName')
  expect({a: 1, b: function () {}}).to.have.respondsTo('b')
})
复制代码

satisfy

断言函数的返回值为true, expect的参数是函数的参数

it('satisfy', function () {
  function bar (n) {
    return n > 0
  }
  expect(1).to.satisfy(bar)
})
复制代码

oneOf

断言目标是否为数组的成员

it('oneOf', function () {
  expect(1).to.oneOf([2, 3, 1])
})
复制代码

change

断言函数执行完成后。函数的返回值发生变化

it('change', function () {
  let a = 0

  function add () {
    a += 3
  }

  function getA () {
    return a
  }

  // 断言getA的返回值发生变化
  expect(add).to.change(getA)
  // 断言变化的大小
  expect(add).to.change(getA).by(3)
})

复制代码

Karma

什么是Karma?

Karma是一个测试工具,能让你的代码在浏览器环境下测试。代码多是设计在浏览器端执行的,在node环境下测试可能有些bug暴露不出来;另外,浏览器有兼容问题,karma提供了手段让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行。若是你的代码只会运行在node端,那么你不须要用karma。

安装, 运行Karma

// 安装karma
npm install karma --save-dev
复制代码
// 在package.json文件中添加测试命令
scripts: {
  test:unit: "karma start"
}
复制代码

经过Karma测试项目, 须要在项目中添加配置karam.conf.js的文件。推荐使用karam init命令生成初始化的配置文件。下面是, karam init 命令的配置项。生成配置文件以后, 就能够经过"npm run test:unit"命令进行单元测试了。

1. Which testing framework do you want to use(使用的测试框架)?
  mocha
2. Do you want to use Require.js(是否使用Require)?
  no
3. Do you want to capture any browsers automatically(须要测试的浏览器)?
  Chrome, IE,
4. What is the location of your source and test files(测试文件的位置)?
  test/*.test.js
5. Should any of the files included by the previous patterns be excluded(须要排除的文件) ?
  node_modules
6. Do you want Karma to watch all the files and run the tests on change(何时开始测试) ?
  change
复制代码

添加断言库

// 安装
npm install --save-dev chai karma-chai 
复制代码

配置karma.conf.js与webpack

若是测试发送在浏览器环境, Karma会将测试文件, 模拟运行在浏览器环境中。因此推荐使用webpack, babel, 对测试文件进行编译操做。Karma中提供了处理文件中间件的配置。ps: 以前因为浏览器环境不支持require, 而我在test文件中使用了require, 而且我没有将测试文件进行编译, 耽误了我半天的时间:(

karma.conf.js配置的更多的细节,能够查看karma文档

// 安装babel
npm install --save-dev karma-webpack webpack babel-core babel-loader babel-preset-env
复制代码
// 对文件添加webpack的配置, 对配置文件使用babel进行处理
module.exports = function(config) {
  config.set({

    basePath: '',

    frameworks: ['mocha', 'chai'],

    files: [
      'test/*.test.js'
    ],

    exclude: [
      'node_modules'
    ],

    preprocessors: {
      'test/*.test.js': ['webpack']
    },

    webpack: {
      // webpack4中新增的mode模式
      mode: "development",

      module: { 
        rules: [
          { test: /\.js?$/, loader: "babel-loader",  options: { presets: ["env"] }, },
        ]
      }
    },

    reporters: ['progress'],

    port: 9876,

    colors: true,

    logLevel: config.LOG_INFO,

    autoWatch: true,

    browsers: ['Chrome'],

    singleRun: false,

    concurrency: Infinity
  })
}
复制代码

编写测试文件

// 经过babel, 浏览器能够正常的解析测试文件中的require
const modeA = require('../lib/a')
const expect = require('chai').expect

describe('test', function () {
  it('example', function () {
    expect(modeA.a).to.equals(1)
  })
})
复制代码

Vue与Karma集成

与处理浏览器中的require同理, 若是咱们须要对.vue文件进行测试, 则须要经过vue-loader的对.vue文件进行处理。

咱们首先经过vue-cli初始化咱们的项目, 这里我使用的是vue-cli2.x的版本, 3.x的版本vue-cli对webpack的配置做出了抽象, 没有将webpack的配置暴露出来, 咱们会很难理解配置。若是须要使用vue-cli3.x集成karma, 则须要另外的操做。

// 安装karma以及karam的相关插件
npm install karma mocha karma-mocha chai karma-chai karma-webpack --save-dev

// 配置karma.conf.js
// webpack的配置直接使用webpack暴露的配置
const webpackConfig = require('./build/webpack.test.conf')

module.exports = function(config) {
  config.set({

    basePath: '',

    frameworks: ['mocha'],

    files: [
      'test/*.test.js'
    ],

    exclude: [
    ],
    
    // 测试文件添加中间件处理
    preprocessors: {
      'test/*.test.js': ['webpack']
    },

    webpack: webpackConfig,

    reporters: ['progress'],

    port: 9876,

    colors: true,

    logLevel: config.LOG_INFO,

    autoWatch: true,

    browsers: ['Chrome'],

    singleRun: false,

    concurrency: Infinity
  })
}
复制代码

图片

⚠️ 注意: 这里依然存在一个问题Can't find variable: webpackJsonp, 咱们能够将webpackConfig文件中的CommonsChunkPlugin插件注释后, karma将会正常的工做。

编写测试

import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import HelloWorld from '../src/components/HelloWorld.vue'

describe('HelloWorld.vue', function () {
  const wrapper = mount(Counter)
  it('Welcome to Your Vue.js App', function () {
    console.log('Welcome to Your Vue.js App')
    expect(wrapper.vm.msg).to.equals('Welcome to Your Vue.js App')
  })
})
复制代码

Vue-cli3与Karma集成

对于vue-cli3我尝试本身添加karma.conf.js的配置, 虽然能够运行,可是存在问题。issue中, 官方建议我在vue-cli3版本中使用vue-cli的karma的插件解决。

对于vue-cli3,能够使用vue-cli-plugin-unit-karma插件, 集成vue-cli3与karma

VueTestUtils

对于VueTestUtils, 我这里并不想作过多的介绍。由于它拥有详尽和完善的中文文档, 在这里我只会作大体的概述文档地址, 值得注意的一点文档中部份内容已通过时, 以及不适用与vue-cli3

什么是VueTestUtils?

VueTestUtils是Vue.js官方的单元测试实用工具库, 提供不少便捷的接口, 好比挂载组件, 设置Props, 发送emit事件等操做。咱们首先使用vue-cli3建立项目, 并添加vue-cli-plugin-unit-karma的插件。而vue-cli-plugin-unit-karma插件已经集成了VueTestUtils工具, 无需重复的安装。

VueRouter

VueRouter, 是Vue的全局插件, 而咱们测试的都是单文件组件, 咱们该如何测试VueRouter的呢?, VueTestUtils为咱们提供了localVue的API, 可让咱们在测试单文件组件的时候, 使用VueRouter。(更多内容请参考文档)

import { mount, createLocalVue } from '@vue/test-utils'
import Test from '../../src/views/Test.vue'
import VueRouter from 'vue-router'

it('localVue Router', function () {
    const localVue = createLocalVue()
    localVue.use(VueRouter)
    const router = new VueRouter()
    // 挂载组件的同时, 同时挂载VueRouter
    const wrapper = mount(Test, {
      localVue,
      router
    })
    // 咱们能够在组件的实例中访问$router以及$route
    console.log(wrapper.vm.$route)
  })
复制代码

对于router以及route, 咱们也能够经过mocks进行伪造, 并注入到组件的实例中

it('mocks', function () {
    // 伪造的$route的对象
    const $route = {
      path: '/',
      hash: '',
      params: { id: '123' },
      query: { q: 'hello' }
    }
    
    const wrapper = mount(Test, {
      mocks: {
        $route
      }
    })
    // 在组件的实例中访问伪造的$route对象
    console.log(wrapper.vm.$route)
  })
复制代码

Vuex

对于Vuex的测试, 咱们须要明确一点咱们不关心这个action或者mutation作了什么或者这个store是什么样子的, 咱们只须要测试action将会在适当的时机触发。对于getters咱们也不关心它返回的是什么, 咱们只须要测试这些getters是否被正确的渲染。更多细节请查看文档。

describe('Vuex', function () {

  // 应用全局的插件Vuex
  const localVue = createLocalVue()
  localVue.use(Vuex)

  let actions
  let store
  let getters
  let isAction = false

  // 在每一个测试用例执行前, 伪造action以及getters
  // 每一个测试用例执行前, 都会重置这些数据
  beforeEach(function () {
    // 是否执行了action
    isAction = false

    actions = {
      actionClick: function () {
        isAction = true
      }, 
      actionInput: function () {
        isAction = true
      }
    }

    getters = {
      name: () => '方方'
    }

    // 生成伪造的store
    store = new Vuex.Store({
      state: {},
      actions,
      getters
    })
  })

  // 测试是否触发了actions
  it('若是text等于actionClick, 触发了actionClick action', function () {
    const wrapper = mount(TestVuex, { store, localVue })
    wrapper.vm.text = 'actionClick'
    // 若是成功触发actionClick action, isAction将为true
    expect(isAction).to.true
  })

  it('若是text等于actionInput,触发actionInput action', function () {
    const wrapper = mount(TestVuex, { store, localVue })
    wrapper.vm.text = 'actionInput'
    // 若是成功触发actionInput action, isAction将为true
    expect(isAction).to.true
  })

  // 对于getters, 同理action
  // 咱们只关注是否正确渲染了getters,并不关心渲染了什么
  it('测试getters', function () {
    const wrapper = mount(TestVuex, {
      store,
      localVue
    })
    // 测试组件中使用了getters的dom, 是否被正确的渲染
    expect(wrapper.find('p').text()).to.equals(getters.name())
  })
})
复制代码
相关文章
相关标签/搜索