上车,手把手在实际项目中做前端单元测试

在实际项目中做前端单元测试

本篇篇幅较长,各位大佬可以收藏再看

在一个已有 Vue CLI 创建的项目中配置 Jest,添加Vue Test Utils

vue add unit-jest

配置Jest

在项目的根目录下找到jest.config.json文件,修改如下(部分配置已注释,可按需打开)

module.exports = {
  preset: "@vue/cli-plugin-unit-jest",
  // 开启测试报告
  collectCoverage: true,
  // 统计哪里的文件
  collectCoverageFrom: ["**/src/views/**", "!**/node_modules/**"],
  // 告诉jest针对不同类型的文件如何转义
  transform: {
    "^.+\.vue$": "vue-jest",
    // '^.+\.(vue)$': '<rootDir>/node_modules/vue-jest',
    '^.+\.js$': '<rootDir>/node_modules/babel-jest',
    '.+\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\.jsx?$': 'babel-jest',
    '^.+\.ts?$': 'ts-jest'
  },
  // 告诉jest需要解析的文件
  moduleFileExtensions: [
      'js',
      'ts',
      'jsx',
      'json',
      'vue'
  ],
  // 告诉jest去哪里找模块资源,同webpack中的modules
  moduleDirectories: [
      'src',
      'node_modules'
  ],
  // 告诉jest在编辑的过程中可忽略哪些文件,默认为node_modules下的所有文件
  transformIgnorePatterns: [
      '<rootDir>/node_modules/'
      + '(?!(vue-awesome|vant|resize-detector|froala-editor|echarts|html2canvas|jspdf))'
  ],
  // 别名,同webpack中的alias
  // moduleNameMapper: {
  //     '^src(.*)$': '<rootDir>/src/$1',
  //     '^@/(.*)$': '<rootDir>/src/$1',
  //     '^block(.*)$': '<rootDir>/src/components/block/$1',
  //     '^toolkit(.*)$': '<rootDir>/src/components/toolkit/$1'
  // },
  // snapshotSerializers: [
  //     'jest-serializer-vue'
  // ],
  // 告诉jest去哪里找我们编写的测试文件
  testMatch: [
      '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
      // '**/tests/unit/**/Test.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  // 在执行测试用例之前需要先执行的文件
  // setupFiles: ['jest-canvas-mock']
};
​
页面结构

要测试的页面数据(此代码只留下重要部分)

<div v-if="numInfo.num">
  <div class="saleCard" v-if="promotionValue.showNumberOrderCode">
   <van-field v-model="orderCode" label="靓号订单查询:">
    <template #button>
     <van-button @click="getSaleCardOrder(1)">查询</van-button>
    </template>
   </van-field>
  </div>
  <div class="saleCard" v-if="promotionValue.showOutCallOrder">
   <van-field v-model="sysOrderId" label="酬金订单查询:">
    <template #button>
     <van-button @click="getSaleCardOrder(2)">查询</van-button>
    </template>
   </van-field>
  </div>
  <van-field v-model.trim="formData.name" label="姓名:" />
  <van-field v-model="formData.contactNo" label="手机号码:" />
  <van-field v-model="formData.authCode" v-if="promotionValue.showVerifyCode" label="验证码:">
   <template #button>
    <van-button @click="getSmsCode">{{codeText}}{{time}}</van-button>
   </template>
  </van-field>
  <van-field v-model="formData.idCardNo" label="身份证号:" v-if="promotionValue.showIdCardNo" />
  <van-field :value="address" label="城市:" />
  <van-field v-model.trim="formData.address" label="详细地址:" />
  <van-field label="支付方式:" label-align="right" v-if="numInfo.salePrice !=='价格面议'&&promotionValue.mergerPay&&numInfo.salePrice !=='0'">
   <template #input>
    <div v-if="promotionValue.payTypes.includes('wx')" />
      <span>微信支付</span>
    </div>
    <div v-if="promotionValue.payTypes.includes('alipay')" />
      <span>微信支付支付宝支付span>
    </div>
   </template>
  </van-field>
  <van-checkbox v-model="checked">
    我已阅读并同意
    <a @click="openPage(1)">《个人信息保护政策》、</a>
    <a @click="openPage(2)">《个人信息收集证明》、</a>
    <a @click="openPage(3)">《单独同意书》、</a>
    <a @click="openPage(4)">《入网许可协议》</a>
  </van-checkbox>
  <img src="../assets/img/btn.gif" class="submitBtm" @click="submit"> // 提交图片
</div>

页面总共有9个输入框和一个复选框,还有一个提交按钮的图片,不考虑显隐条件,在页面默认情况界面如下:

在这里插入图片描述

因为页面有判断条件,所以v-if判断之后只剩4个输入框。

书写测试用例
1. 找到tests/unit/example.spec.js

因为整个页面是通个一个变量控制的,所以测试有号码的情况下,页面元素展示是否正确

import { mount } from '@vue/test-utils'
import PageForm from "@/views/pageForm.vue";
​
import Vue from 'vue';
import Util from '@/utils';
Vue.prototype.$util = Util;
​
const Vant = require('vant')
// import Vant from 'vant';
Vue.use(Vant);
​
describe('PageForm.vue', () => {
  const wrapper = mount(PageForm)
  it('renders vanField number', async () => {
    await wrapper.setData({'numInfo': {num : 1305555555}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(3)
  })
})

通过mount挂载页面

import Util from ‘@/utils’; Vue.prototype.$util = Util; 全局使用公共函数

通过import导入vant会报错,暂时使用require方式

setData 设置页面数据,async和await获取更新后的页面

通过findAll获取全部class类名的标签,toHaveLength断言数量

运行单元测试:

npm run test:unit

结果如下:

在这里插入图片描述

测试通过

2.显示第一个隐藏的靓号订单查询

再增加一个测试方法

...
​
describe('PageForm.vue', () => {
  const wrapper = mount(PageForm)
  it('renders vanField number', async () => {
    await wrapper.setData({'numInfo': {num : 1305555555}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(4)
  })
    
   it('renders vanField showNumberOrderCode', async () => {
    await wrapper.setData({'promotionValue': {showNumberOrderCode : true}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(5)
  })
})

再次运行单元测试:

npm run test:unit

结果如下:

在这里插入图片描述

最后,两个测试都通过了

3.显示酬金订单查询
...
​
describe('PageForm.vue', () => {
  const wrapper = mount(PageForm)
  it('renders vanField number', async () => {
    await wrapper.setData({'numInfo': {num : 1305555555}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(4)
  })
    
   it('renders vanField showNumberOrderCode', async () => {
    await wrapper.setData({'promotionValue': {showNumberOrderCode : true}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(5)
  })
   it('renders vanField showOutCallOrder', async () => {
    await wrapper.setData({'promotionValue': {showOutCallOrder : true}})
    expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(6)
  })
})

运行单元测试:

npm run test:unit

结果如下:

在这里插入图片描述

三个测试用例都通过了,其他隐藏的输入框的测试用例和上面一样

可配置用例
  1. 在tests目录下新建testData.js文件(准备了两组测试数据,自行粘贴)
const rendersVanField = [
  {
    arr: [
      {
        key: 'showNumberOrderCode',
        bool: false, // 靓号订单查询
      },
      {
        key: 'showOutCallOrder',
        bool: true, // 酬金订单查询
      },
      {
        key: 'showVerifyCode',
        bool: false, // 验证码
      },
      {
        key: 'showIdCardNo',
        bool: false, // 身份证号
      },
      {
        key: 'mergerPay',
        bool: false, // 支付方式
      },
    ],
    expected: 5 // 期望值
  },
  {
    arr: [
      {
        key: 'showNumberOrderCode',
        bool: true, // 靓号订单查询
      },
      {
        key: 'showOutCallOrder',
        bool: true, // 酬金订单查询
      },
      {
        key: 'showVerifyCode',
        bool: true, // 验证码
      },
      {
        key: 'showIdCardNo',
        bool: true, // 身份证号
      },
      {
        key: 'mergerPay',
        bool: true, // 支付方式
      },
    ],
    expected: 9 // 期望值
  },
]
​
​
export {
  rendersVanField,
}
  1. 修改测试用例
import { mount } from '@vue/test-utils'
import PageForm from "@/views/pageForm.vue";
​
import Vue from 'vue';
import Util from '@/utils';
Vue.prototype.$util = Util;
​
const Vant = require('vant')
// import Vant from 'vant';
Vue.use(Vant);
​
import { rendersVanField } from "../testData";
​
rendersVanField.forEach((el, i) => {
  describe(`PageForm${i}.vue`, () => {
    const wrapper = mount(PageForm)
    it('renders vanField number', async () => {
      await wrapper.setData({'numInfo': {num : 13073072255}})
      // el.arr.forEach(async (item) => {
      //   await wrapper.setData({'promotionValue': {[item.key] : item.bool}})
      // })
      for (let index = 0; index < el.arr.length; index++) {
        await wrapper.setData({'promotionValue': {[el.arr[index].key] : el.arr[index].bool}})
      }
      expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(el.expected)
    })
  })
})

里面这层循环不用forEach而用for,是出现了循环之后设置的数据没有生效,所以改用for

运行单元测试:

npm run test:unit

结果如下:

在这里插入图片描述

结果显示两条单元测试只通过了一条,为什么第二条测试少了一个输入框呢?查看结构发现支付状态是有多个条件判断显示的

 <van-field label="支付方式:" label-align="right" v-if="numInfo.salePrice !=='价格面议'&&promotionValue.mergerPay&&numInfo.salePrice !=='0'">

修改单元测试

...

import { rendersVanField } from "../testData";

rendersVanField.forEach((el, i) => {
  describe(`PageForm${i}.vue`, () => {
    const wrapper = mount(PageForm)
    it('renders vanField number', async () => {
      await wrapper.setData({'numInfo': {num : 13073072255}})
      for (let index = 0; index < el.arr.length; index++) {
        if ([el.arr[index].key] === 'mergerPay') {
          await wrapper.setData({
            'promotionValue': {[el.arr[index].key] : el.arr[index].bool},
            'numInfo': {salePrice: '100'}
          })
        } else {
          await wrapper.setData({'promotionValue': {[el.arr[index].key] : el.arr[index].bool}})
        }
      }
      expect(wrapper.findAll('[class="van-cell__title van-field__label van-field__label--right"]')).toHaveLength(el.expected)
    })
  })
})

最后运行单元测试,可以发现两条都通过了

在这里插入图片描述

4.模拟Axios模块

项目中有与后端接口交互获取数据的清空,结合这个页面发现有一个变量判断显示,但是上面用例的显示是写死的,在这里可以改造成通过接口获取数据

在这里插入图片描述

在src/api目录下新建testAxios.js文件

// /src/api/testAxios
const axios = require("axios");
 
function fetchNumInfoData() {
  return axios
    .get("/numInfo")
    .then((res) => res.data);
}
 
module.exports = {
  fetchNumInfoData,
};

现在我想要测试fetchUserData函数获取数据,但是又并不实际请求接口时,可以使用jest.mock来模拟axios模块

...
const numInfo = require("@/api/testAxios.js");
const axios = require("axios");
jest.mock('axios');

rendersVanField.forEach((el, i) => {
  describe(`PageForm${i}.vue`, () => {
    const wrapper = mount(PageForm)
    
    it('should fetch numInfo', async () => {
      const numInfoData = {
        num: 13073072255,
      };
      
      const res = { data: numInfoData };
      axios.get.mockResolvedValue(res);
      return numInfo.fetchNumInfoData().then(async (resp) => {
        await wrapper.setData({'numInfo': numInfoData})
        expect(resp).toEqual(numInfoData);
      });
    })
    
    it('renders vanField number', async () => {
        await wrapper.setData({'numInfo': {num : 13073072255}})
        ...
    })
  })
})

一旦我们对模块进行了模拟,我们可以用get函数提供的一个mockResolvedValue方法,以返回我们需要测试的数据;通过模拟后,达到了我想要的效果,axios实际上并没有去真正发送请求去获取/numInfo的数据

在这里插入图片描述

运行单元测试,两组数据中四条用例都通过了

5.测试点击提交是否勾选同意协议

新增测试用例submit agreement,因为是新的describe,所以需要复制一个前面的显示页面的用例,否则页面初始化是隐藏的,获取不到元素

...
rendersVanField.forEach((el, i) => {
...
})

describe(`PageFormSubmit.vue`, () => {
  const wrapper = mount(PageForm)

  it('should fetch numInfo', async () => {
    const numInfoData = {
      num: 13073072255,
    };
    
    const res = { data: numInfoData };
    axios.get.mockResolvedValue(res);
    return numInfo.fetchNumInfoData().then(async (resp) => {
      await wrapper.setData({'numInfo': numInfoData})
      expect(resp).toEqual(numInfoData);
    });
  })

  it('submit agreement', async () => {
    const submitBtn = wrapper.find('[class="submitBtm"]')
    await submitBtn.trigger("click")
    expect(wrapper.vm.checked).toBe(true)
  })
})

运行单元测试,发现与预期一致

在这里插入图片描述

6.测试表单提交校验方法

首先在utils下新建一个form-check.js文件,写入校验方法

const ruleList = {
  phone: value => {
    if (!value) return '请输入手机号';
    if (/^1[0-9]{10}$/.test(value)) return true;
    return '手机号码不正确'
  },
  verifyCode: value => {
    if (!value) return '请输入验证码';
    if (value.length === 4 || value.length === 6) return true;
    return "验证码错误";
  },
  name: value => {
    if (!value) return '请输入姓名';
    if (/^[\u4e00-\u9fa5]{2,20}$/.test(value)) return true;
    if (value.length < 2 || value.length > 20) return '姓名长度不能小于2或超过20';
    return '姓名必须为汉字'
  },
  idCard: value => {
    if (!value) return '请输入身份证号';
    if (/(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/.test(value)) return true;
    return '请输入正确的身份证号码'
  },
  address: value => {
    if (!value) return '请输入详细地址';
    //地址信息不得含特殊字符
    let roadReg = /^[\u4E00-\u9FA5A-Za-z0-9_—()()-]+$/gi;
    if (!roadReg.test(value)) return '地址信息不得含特殊字符哟';
    let roadReg2 = /^[A-Za-z0-9]+$/gi;
    if (roadReg2.test(value)) return '地址信息不得纯为英文字母或数字哟';
    if (value.length < 4) return '地址不能太短哟';
    return true;
  },
  city: value => {
    if (!value) return '请选择城市';
    return true;
  },
  birthDate: value => {
    if (!value) return '请选择生日';
    return true;
  },
  agreement: value => {
    if (!value) return '请勾选同意相关协议';
    return true;
  },
  agreementCity: value => {
    if (!value) return '请勾选同意相关协议';
    return true;
  },
  agreementRegion: value => {
    if (!value) return '请勾选同意相关协议';
    return true;
  },
  randomCode: (value, item) => {
    if (!value) return '请输入验证码';
    if (item.value.toUpperCase() !== item.codeValue.toUpperCase()) {
      item.getCode()
      return '验证码错误';
    }
    return true
  }
}

export default ruleList

编写第一个校验名字用例

....
import checkForm from '@/utils/form-check'

describe(`PageFormSubmit.vue`, () => {
  ...
  it('submit checkForm', async () => {
    expect(checkForm.name('刘二牛')).toBe(true)
  })
  
})

运行单元测试,校验通过:

在这里插入图片描述

再输入一个错误的数据测试

....
import checkForm from '@/utils/form-check'

describe(`PageFormSubmit.vue`, () => {
  ...
  it('submit checkForm', async () => {
    expect(checkForm.name('1231')).toBe(true)
  })
  
})

没有意外,没通过

在这里插入图片描述

但返回值是不确定的汉字,不方便测试添加测试数据,所以可以稍微改造一下

....
import checkForm from '@/utils/form-check'

describe(`PageFormSubmit.vue`, () => {
  ...
  it('submit checkForm', async () => {
    expect(checkForm.name('1231')).not.toBe(true)
  })
  
})

这样是可以通过的

在这里插入图片描述

测试表单的其他方法

....
import checkForm from '@/utils/form-check'

describe(`PageFormSubmit.vue`, () => {
  ...
  it('submit checkForm', async () => {
    expect(checkForm.name('刘二牛')).toBe(true)
    expect(checkForm.phone('15966523256')).toBe(true)
    expect(checkForm.address('广州天河区xxx')).toBe(true)
    expect(checkForm.verifyCode('1311')).toBe(true)
    expect(checkForm.idCard('22222219990218481X')).toBe(true)
  })
  
})

下图所示,正确的数据都通过了测试

在这里插入图片描述

7.编写可配置测试数据

在testData.js新增测试数据

...
const formData = [
  [
    {
      key: 'name',
      value: '刘二牛',
      expected: true // 期望值
    },
    {
      key: 'phone',
      value: '刘二牛',
      expected: false // 期望值
    },
    {
      key: 'address',
      value: '广州天河区xxx',
      expected: true // 期望值
    },
    {
      key: 'verifyCode',
      value: '1311',
      expected: true // 期望值
    },
    {
      key: 'idCard',
      value: '22222219990218481X',
      expected: true // 期望值
    },
  ],
]

export {
  rendersVanField,
  formData,
}

修改用例:

....
import checkForm from '@/utils/form-check'
import { rendersVanField, formData } from "../testData";

describe(`PageFormSubmit.vue`, () => {
  ...
  formData.forEach((el, i) => {
    it(`submit checkForm${i}`, async () => {
      el.forEach(item => {
        if (item.expected) {
          expect(checkForm[item.key](item.value)).toBe(item.expected)
        } else {
          expect(checkForm[item.key](item.value)).not.toBe(item.expected)
        }
      })
    })
  })
})

结果如下:

在这里插入图片描述

总结

前端框架迭代不断,一个健壮的前端项目应该有单元测试的模块,保证了我们的项目代码质量和功能的稳定;但是也并不是所有的项目都需要有单元测试,毕竟编写测试用例也需要成本;因此如果项目符合下面的几个条件,就可以考虑引入单元测试:

  • 长期稳定的项目迭代,需要保证代码的可维护性和功能稳定;
  • 页面功能相对来说说比较复杂,逻辑较多;
  • 对于一些复用性很高的组件,可以考虑单元测试;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨小凹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值