使用Jest对Vue进行自动化测试

上一篇文章中,讲解了 Jest 的一些基本用法,理所当然的,我们需要应用到实际项目中,这里以 Vue 举例,介绍一些 Jest 在 Vue 开发中的基本用法。

TDD vs BDD

在开始之前,我们需要先了解两种编写测试用例的方式,以便在实际开发中选取合适的方式。

Test Driven Development (TDD) 测试驱动开发

TDD 的原理就是在编写代码之前先编写测试用例,由测试来决定我们的代码,而且 TDD 更多的需要编写独立的测试用例,比如只测试一个组件的某个功能点,某个工具函数等。它是白盒测试。

开发流程大致是:编写测试用例、运行测试、编写代码使测试通过、优化代码。

TDD 的优势:从长期来看,可以有效减少回归测试的 Bug;因为先编写测试,所以可能出现的问题都被提前发现了;测试覆盖率高,因为后编写代码,因此测试用例基本都能照顾到;保证代码质量。

TDD 的劣势:因为侧重点在于代码,更多是保证某个测试单元没问题,因此无法保证业务流程没有问题;而且需求经常变更,在修改某个功能点之前要先修改测试用例,因此在复杂的项目中工作量很大;测试代码和实际代码可能会出现耦合,经常需要修改。

BDD (Behavior Driven Development) 行为驱动开发

BDD 是从产品角度出发,它鼓励开发人员和非开发人员之间的协作,是一种黑盒测试。

开发流程大致是:获悉需求并编写代码,然后再从用户角度编写集成测试。

BDD 的优势:它的测试重点更多是站在项目角度,在 UI 和 DOM 的角度进行测试,直接地测试业务流程是否没问题,测试代码和实际代码解耦。

BDD 的劣势:因为是集成测试,因此不是那么关注每个函数功能,测试覆盖率比较低,没有 TDD 那么严格的保证代码质量。

Vue 中配置 Jest

在这里,直接借助了 Vue CLI 工具来初始化项目,在初始化时会询问是否使用单元测试,我们只需要按照步骤选择,并选择 Jest 即可。

init.png

通过这种方式 Vue 会内置 Vue Test Utils 帮助我们进行测试

我们可以打开package.json文件修改"test:unit": "vue-cli-service test:unit"在后面加上--watch这样就只测试发生变动的文件

安装后,项目目录下会有一个jest.config.js文件,里面放的是 jest 相关的配置,我们可以根据自己的需要修改之

module.exports = {
  // 依次找 js、jsx、json、vue 后缀的文件
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    'vue'
  ],
  // 使用 vue-jest 帮助测试 .vue 文件
  // 遇到 css 等转为字符串 不作测试
  // 遇到 js jsx 等转成 es5
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.jsx?$': 'babel-jest'
  },
  // 哪些文件下的内容不需要被转换
  transformIgnorePatterns: [
    '/node_modules/'
  ],
  // 模块的映射 @ 开头到根目录下寻找
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  // snapshot 怎么去存储
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  // npm run test:unit 时到哪些目录下去找 测试 文件
  testMatch: [
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  // 模拟的浏览器的地址是什么
  testURL: 'http://localhost/',
  // 两个帮助使用 jest 的插件 过滤 测试名/文件名 来过滤测试用例
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname'
  ]
}

为了方便,修改了testMatch的配置:

  // 这条是自己新增的,测试 .vue 文件的测试覆盖率
  // vue-cli-service test:unit --coverage 可以生成测试覆盖率文件
  // 可以在 package.json 作如下配置
  // "test:coverage": "vue-cli-service test:unit --coverage"
  collectCoverageFrom: ['**/*.{vue}', '!**/node_modules/**'],
  testMatch: [
    // 进行测试时匹配 __tests__ 目录下的 js/jsx/ts/tsx 文件
    '**/__tests__/**/*.(js|jsx|ts|tsx)'
  ],
  // .eslintrc.js 不需要进行测试
  testPathIgnorePatterns: [
    '.eslintrc.js'
  ],

同时我们要明确开发的目录结构,好的目录组织可以帮助开发者更好的理解代码,以下是一种参考,当然可以有别的方式,只要组织得容易理解就好

├── App.vue
├── assets
├── components # 基础组件
├── views  # 项目主页面
│   └── Article # Article 页面
│       ├── ArticleHome.vue # Article 页面组件
│       ├── __mocks__
│       │   └── axios.js # 需要 mock 的文件
│       ├── __tests__ # 测试目录
│       │   ├── integration # 集成测试目录
│       │   │   └── Article.js
│       │   └── unit # 单元测试目录
│       │       └── store.js
│       └── components  # 页面子组件
│           ├── MyHeader.vue
│           └── MyFooter.vue
├── main.js
├── store.js # Vuex store
└── utils # 工具函数

下面介绍几种基础的用法,更详细的可以看官方文档

使用 Jest 测试 Vue 项目

判断 DOM 结构是否发生改变

Vue-test-utils提供的shallowMount可以帮助我们挂载组件,但它不挂载子组件,在组件单元测试中特别有用

另外mount方法则是把子组件也进行挂载

返回的wrapper包含了所测试组件的属性以及 vnode,我们可以借助它来测试 Vue

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

// __test__/unit/MyHeader.js
it('提示 MyHeader 样式是否有发生变更', () => {
  const wrapper = shallowMount(MyHeader)
  expect(wrapper).toMatchSnapshot()
})

input

为了获取 DOM,通常需要添加data-test=""属性来获取

我们可以编写一个webpack plugin在生成production环境代码的时候移除之

<!-- MyHeader.vue -->
<input
  data-test="input"
  v-model="inputValue"
  @keyup.enter="add"
/>
// __test__/unit/MyHeader.js

// 为了让代码看起来更清晰,我们可以使用 describe 包裹起来

describe('MyHeader 组件测试', () => {
  it('input 测试', () => {
    // 挂载 MyHeader 组件
    const wrapper = shallowMount(MyHeader)
    // 判断是否存在 input
    // wrapper.findAll('[data-test="input"]').at(0) 取第一个元素
    const input = wrapper.find('[data-test="input"]')
    expect(input.exists()).toBe(true)

    // input 一开始内容为空
    const inputValue = wrapper.vm.inputValue
    expect(inputValue).toBe('')

    // 模拟输入了内容
    input.setValue('name')

    // 模拟触发回车事件
    input.trigger('keyup.enter')

    // 模拟向外发送了一个 add 事件
    expect(wrapper.emitted().add).toBeTruthy()

    // 模拟回车之后内容为空
    expect(wrapper.vm.inputValue).toBe('')
  })
})

集成测试

相比于单元测试,集成测试从业务流程角度出发,同时测试多个组件,保证整个用户行为是没有问题的。


it(`
  1. 在 input 输入框输入内容
  2. 点击回车按钮
  3. 增加用户输入内容的列表项
`, () => {
  const wrapper = mount(TodoList)
  const inputElem = wrapper.findAll('[data-test="input"]').at(0)
  const content = '今晚去踢波'
  inputElem.setValue(content)
  inputElem.trigger('change')
  inputElem.trigger('keyup.enter')

  // 这是从另外一个组件中获取的
  const listItems = wrapper.findAll('[data-test="list-item"]')
  expect(listItems.length).toBe(1)
  expect(listItems.at(0).text(0)).toContain(content)
})

Vuex

测试 Store

import store from '../../../../store'

it('当 store commit change 时 value 发生变化', () => {
  const value = 'content'
  store.commit('change', value)
  expect(store.state.value).toBe(value)
})

组件中

// 在 mount 时把 store 传入,这样就能使用 store
const wrapper = mount(TodoList, { store })

异步测试

在组件挂载时,我们经常会加载远程数据来渲染页面,这样,渲染出来后的 DOM 就不是可以立即能获取的,因此应该这样测试:

// 因为使用到 nextTick,因此需要传入 done 参数
it(`
  1. 用户打开页面时,请求远程数据
  2. 页面渲染远程数据
`, (done) => {
  const wrapper = mount(TodoList, { store })

  // 只需要稍有延迟就能取到数据
  wrapper.vm.$nextTick(() => {
    // 此时可以获取到渲染后的 DOM
    const listItems = wrapper.findAll('[data-test="list-item"]')
    expect(listItems.length).toBe(2)
    // 当 done 被执行才结束测试
    done()
  })
})

mock 和 timer

假如我们使用了 axios,我们可以使用手动 mock 的方式,不用真正地请求数据,而是重写获取数据的实现,这样可以省去每次远程获取数据的时间。

// __mocks__/axios.js
const response = {
  errorCode: 0,
  data: [{ id: 0, name: 'name' }]
}

export default {
  get () {
    if (url === '/getData') {
      return new Promise((resolve, reject) => {
        if (this.errorCode === 0) {
          resolve(response)
        } else {
          reject(new Error())
        }
      })
    }
  }
}

// TodoList.vue
mounted() {
  setTimeout(() => {
    axios.get('/getData').then((res) => {
      this.data = res.data
    }).catch(e => {
      
    })
  }, 5000)
}

通过上面的 mock,当我们请求/getData接口时就会先找到 mock 中模拟的请求,并返回模拟请求的中的数据

接下来就可以直接在测试中使用了,在这里再加了一个 5s 的延时,以演示如何测试有 timer 的情况

// 对 setTimeout 的统计都清零,避免测试之间相互影响
// 进入导致 toHaveBeenCalledTimes 得不到预期值
beforeEach(() => {
  jest.useFakeTimers()
})

it(`
  1. 用户打开页面时,等待 5s,然后请求远程数据
  2. 页面渲染远程数据
`, (done) => {
  const wrapper = mount(TodoList, { store })

  // 希望 setTimeout 被调用一次
  // 也可以使用 jest.advanceTimersByTime(5000) 来前进多少秒
  expect(setTimeout).toHaveBeenCalledTimes(1)

  // 让 timer 立即执行
  jest.runAllTimers()

  // 获取远程数据,在 nextTick 后把数据渲染出来
  wrapper.vm.$nextTick(() => {
    const listItems = wrapper.findAll('[data-test="list-item"]')
    expect(listItems.length).toBe(2)
    done()
  })
})

如果要测试请求出现失败的情况,我们可以这样做:

import axios from '../../__mocks__/axios'

beforeEach(() => {
  // 每个测试用例之前,都把请求设为成功
  axios.errorCode = 0
})

it(`测试失败`, () => {
  // 在这个测试用例中,把请求设为失败
  // 错误码是自己定的
  axios.errorCode = 10000
})

小结

我们需要明确单元测试、集成测试、TDD、BDD几个概念,针对不同的情况使用不同的测试方式,比如测试工具函数可以用 TDD 的测试方式,面对复杂的项目,我们需要保证用户的体验,就可以使用 BDD 的测试方式,他们之间不是对立的,我们可以在项目中灵活地使用它们,把它拓展到团队中,让自动化测试达到最佳实践。如果测试写得好,那么测试本身就已经是一份文档了,能保证项目在迭代中的代码质量,在多人协同开发中特别有用。

官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352