实例展示vue单元测试及难题解惑

通过生动详实的例子带你排遍vue单元测试过程中的所有疑惑与难题。

技术栈:jest、vue-test-utils。

共四个部分:运行时、Mock、Stub、Configuring和CLI。

运行时

在跑测试用例时,大家的第一个绊脚石肯定是各种undifned报错。

解决这些报错的血泪史还历历在目,现在总结来看,大都是缺少运行时变量抑或异步造成的。

这里咱们只说运行时,基本就这两类:

1.缺少window等环境变量

一般通过引入global-jsdom解决,这也是官方推荐的。当然我们也可以自己在测试代码中直接声明定义。

比如我们在业务代码中使用了sessionStorage。

  1. // procudtpay.vue

  2. <script>

  3. const sessionParams = window.sessionStorage.getItem('sessionParams')

  4. export default {

  5. data () { }

  6. }

  7. </script>

然后在测试代码中直接重定义,这样在运行时,实际取到的值就是我们在这里定义的。

  1. // procudtpay.spec.js

  2. window.sessionStorage = {

  3. getItem: () => {

  4. return { name:'name', type:'type' }

  5. }

  6. }

  7. import procudtpay from '../views/procudtpay.vue'

这里关于执行顺序做一点额外说明:

示例中sessionParams的赋值是在import引入.vue模块就执行了的,所以对sessionStorage的定义赋值需要在引入之前。

如果你的sessionStorage取值是在vue实例化后,比如created中,那么则没有该问题。

2.缺少在main.js中定义/注册的全局属性和方法

这些就需要在测试代码中引入同款,以及通过mount的配置项mocks和stubs,分别对其进行mock或者存根了。

  1. // main.js

  2. import Vue from 'vue'

  3. import Mint from 'mint-ui'

  4. import '../filter'

  5. import axios from 'axios'

  6. Vue.use(Mint)

  7. Vue.prototype.$post = (url, params) => {

  8. return axios.post(url, params).then(res => res.data)

  9. }

  10. Vue.filter('filterxxx', function (value) {

  11. // bala bala ba…

  12. })

  13. // xxx.spec.js

  14. import Vue from 'vue'

  15. import '../../filter/filter' // 引入注册同款过滤器

  16. Vue.filter('filterxxx', function (value) {

  17. // bala bala ba…

  18. })

  19. import { $post } from './http.js'

  20. it('快照测试', () => {

  21. const wrapper = shallowMount(ProductPay, {

  22. mocks: {

  23. $post // 用自己定义的mock数据取代真实http请求

  24. },

  25. stubs:['mt-header'] // 存根组件

  26. })

  27. // ...

  28. })

通常其他测试文件也会依赖这些全局变量,我们可以通过配置jest的setupFiles实现复用。

Mock

我翻开代码一看,这代码没有注释,歪歪斜斜的每一行都写着‘断言正确’四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满屏都写着两个字:“造假”!

正应了那一句:人(ce)生(shi)如戏,全靠演技(mock)。总之,mock老重要了。

1.mock简单函数

我们从最简单的mock一个函数开始。

比如我们现在想要测试:当用户购买成功,期望页面能跳转到结果页。

  1. // productpay.vue

  2. <script>

  3. export default {

  4. ...

  5. methods:{

  6. commmit () {

  7. this.$post('xxx', params).then(data => {

  8. this.$router.push(`/payresult`)

  9. })

  10. }

  11. }

  12. }

  13. </script>

那么,我们可以通过mock掉$router的push方法,然后断言它有被调用且参数正确,达成测试目的。

  1. // productpay.spec.js

  2. it('当用户购买成功后,页面应该跳转至结果页', async () => {

  3. const mockFunc = jest.fn()

  4. const wrapper = shallowMount(ProductPay, {

  5. mocks: {

  6. $post,

  7. $router: {

  8. push: mockFunc

  9. }

  10. }

  11. })

  12. wrapper.vm.commmit() // 提交购买

  13. expect(mockFunc).toHaveBeenCalledWith('/payresult')

  14. })

2.mockHttp请求,指定返回结果

http请求和上面例子中的$router的区别是,它需要返回值。jest有多种方式指定返回值,这里用的是mockImplementation。

  1. // test/**.spec.js

  2. it('当用户xxxx,应该xxxx', async () => {

  3. const respSuccess = { data: [...], code:0 }

  4. const respError = { data: [...], code:888 }

  5. // 定义mock函数

  6. const mockPost = jest.fn()

  7. const wrapper = shallowMount(index, {

  8. mocks: {

  9. $post:mockPost // 应用该mock函数

  10. }

  11. })

  12. // 指定异步返回数据

  13. mockPost.mockImplementation(() => Promise.resolve(respError))

  14. // 可以对调用情况进行断言

  15. expect(mockPost).toHaveBeenCalled()

  16. mockPost.mockImplementation(() => Promise.resolve(respSuccess))

  17. //也可以等待异步结束,对结果进行断言

  18. await flushPromises()

  19. expect(wrapper.vm.list).toEqual(respSuccess.data)

  20. })

实际上我们项目中调用的接口会很多,且不乏返回大量数据的情况。如果这些都定义在测试代码里就会很臃肿。这时候,我们可以对该功能做个简单的模块化。

  1. // 常见的业务代码

  2. // main.js中把axios挂载到了vue实例

  3. Vue.prototype.$post = (url, params) => {

  4. return axios.post(url, params).then(res => res.data)

  5. }

  6. // Index.vue中的请求

  7. getProductList () {

  8. this.$post('/ProductListQry', {}).then(data => {

  9. this.ProductList = data.List

  10. })

  11. }

  12. // 1. 在单独js中存放模拟数据 data/ProductListQry.js

  13. export default {

  14. data:[{ id:1,name:'name',...},...],

  15. code:0

  16. }

  17. // 2. 定义post方法,并做个数据匹配 test/http.js

  18. import ProductListQry from '@/data/ProductListQry.js'

  19. const mockData = {

  20. ProductListQry,

  21. ... //可以用同样的方式引入更多mock数据

  22. }

  23. const $post = (url = '') => {

  24. return new Promise((resolve, reject) => {

  25. const jsName = String(url).split('/')[1]

  26. resolve(mockData[jsName])

  27. })

  28. }

  29. export { $post }

  30. // 3. 引入并使用 test/index.spec.js

  31. import Index from '@/views/Index.vue'

  32. import { $post } from './http.js'

  33. it('...',()=>{

  34. const wrapper = shallowMount(Index, {

  35. mocks: {

  36. $post

  37. }

  38. })

  39. wrapper.vm.getProductList() //触发请求

  40. await flushPromises() //等待异步请求结束

  41. //可以看到wrapper中就有了我们指定的模拟数据

  42. console.log(wrapper.vm.ProductList)

  43. })

同理,如果要测试请求失败的情形,可以再定义一个返回错误数据的方法,比如就叫$postError。

  1. // test/**.spec.js

  2. import { $postError } from './http.js'

  3. it('...',()=>{

  4. const wrapper = shallowMount(Index, {

  5. mocks: {

  6. $post:$postError

  7. }

  8. })

  9. wrapper.vm.getProductList() //触发请求

  10. await flushPromises() //等待异步请求结束

  11. // 我们就可以就获取到错误数据的场景进行测试了

  12. console.log(wrapper.vm.ProductList)

  13. })

3.mock整个模块

当业务代码中直接使用了引入的组件/方法时,我们对其测试可能就需要mock整个模块。下面是一个用弹窗做表单验证的场景:

  1. // productpay.vue

  2. <script>

  3. import { MessageBox } from '../Component'

  4. export default {

  5. methods:{

  6. makeSurebuy () {

  7. let payAmount = delcommafy(this.payAmount)

  8. if (!payAmount) {

  9. MessageBox({

  10. message: '请先输入购买金额'

  11. })

  12. return

  13. }

  14. if (payAmount < this.resData.BaseAmt) {

  15. MessageBox({

  16. message: '购买金额不能小于起存金额'

  17. })

  18. return

  19. }

  20. if (payAmount > this.Balance) {

  21. MessageBox({

  22. message: '购买金额不能大于可用余额'

  23. })

  24. return

  25. }

  26. // 校验通过,发起交易...

  27. }

  28. }

  29. }

  30. <script>

  1. //productpay.spce.js

  2. import Component from '../Component'

  3. jest.mock('../../../components/ZyComponent')

  4. it('当用户点击购买按钮,如果输入非法金额,应该有相应的错误提示', async () => {

  5. wrapper.findAll('.btn-commit').at(0).trigger('click')

  6. expect(Component.MessageBox.mock.calls[0][0])

  7. .toEqual({ message: '请先输入购买金额' })

  8. wrapper.setData({payAmount: '100'})

  9. wrapper.findAll('.btn-commit').at(0).trigger('click')

  10. expect(Component.MessageBox.mock.calls[1][0])

  11. .toEqual({ message: '购买金额不能小于起存金额' })

  12. wrapper.setData({payAmount: '100000000000000000'})

  13. wrapper.findAll('.btn-commit').at(0).trigger('click')

  14. expect(Component.MessageBox.mock.calls[2][0])

  15. .toEqual({ message: '购买金额不能大于可用余额' })

  16. })

我们通过jest.mock()mock整个模块,当该模块的方法被调用后它就会有一个mock属性,可以通过ZyComponent.ZyMessageBox.mock进行访问,其中ZyComponent.ZyMessageBox.mock.calls会返回被调用情况的数组,我们可以根据这个数据对函数被调用次数、入参情况进行断言测试。

Stub存根组件

进行单元测试,理论上我们不用、也不应该在它的测试用例中测试子组件,不然就叫集成测试了。vue-test-utils是通过配置stubs实现对组件mock的。

  1. const wrapper = shallowMount(index, {

  2. stubs: ['mt-header', 'mt-loadmore']

  3. }

但是业务中难免会有调用子组件方法的时候,比如说mint-ui的loadmore。

  1. // procuctlist.vue

  2. <script>

  3. export default {

  4. ...

  5. methods:{

  6. getProductList () {

  7. this.$post('xxx', params).then(data => {

  8. ...

  9. this.ProductList = this.ProductList.concat(data.List)

  10. this.$refs.loadmore.onBottomLoaded()

  11. })

  12. }

  13. }

  14. }

  15. </script>

这时候我们是可以改用mount方法使页面渲染子组件,这样通过$refs就能正常的获取到子组件实例。但更合适的做法应该是自定义存根组件的内部实现,以满足测试需求。

  1. // procuctlist.spec.js

  2. it('当用户上拉产品列表,应该能看到的更多的产品', () => {

  3. const mockOnBottomLoaded = jest.fn()

  4. const mtLoadMore = {

  5. render: () => { },

  6. methods: {

  7. onBottomLoaded: mockOnBottomLoaded

  8. }

  9. }

  10. const mtHeader = {

  11. render: () => { }

  12. }

  13. const wrapper = shallowMount(Index, {

  14. stubs: { 'mt-loadmore': mtLoadMore, 'mt-header': mtHeader },

  15. mocks: {

  16. $post

  17. }

  18. })

  19. const currentPage = wrapper.vm.currentPage

  20. wrapper.vm.loadMoreProduction()

  21. expect(wrapper.vm.currentPage).toEqual(currentPage + 1)

  22. expect(mockOnBottomLoaded).toHaveBeenCalled()

  23. })

最后提一嘴,存根组件后,业务代码中子组件还是会被引入的,只是没有被实例化和渲染。

Configuring和CLI

1.统计代码覆盖率忽略某些文件

使用coveragePathIgnorePatterns配置即可,把这个列出来是应为我遇到两个项目相同配置,有一个死活不生效的问题。最后才从官方文档中得知是babel插件istanbul问题。目前还未解决,只是粗暴的在.balelrc中把istanbul去掉了。有真正解决方案的大佬,留言教下……跪谢。

  1. // jest.config.js

  2. {

  3. coveragePathIgnorePatterns: ['<rootDir>/src/assets/']

  4. }

2.通过t模式,可以仅执行指定的测试用例

当测试用例写的多了,每次执行跑一堆用例,效率很低,如果代码里有很多console,那就更难受了,找个报错都能找半天。当时就想如果能仅测试当前用例就好了。

然后就找到了t模式,jest命令带–watch参数进入监听模式,然后输入t,再输入匹配规则即可。世界一下子就清净了,舒服……

  1. // package.json

  2. {

  3. "scripts":{

  4. "tets":"jest --watch"

  5. }

  6. }

3.vue-awesome-swiper测试运行时报错

如果组件中引入了swiper,那么在执行测试用例时,vue-awesome-swiper中的js会报错,引用即报错,且是第三方代码。

最后通过把swiper组件由局部注册改为全局注册得以解决。

总结:

感谢每一个认真阅读我文章的人!!!

作为一位过来人也是希望大家少走一些弯路,如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助。

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

 

          视频文档获取方式:
这份文档和视频资料,对于想从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!以上均可以分享,点下方小卡片即可自行领取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值