php免费企业网站源码,深圳企业网站建设公司哪家好,免费网站建设可信赖,广州黄埔网站制作目录 前情安装依赖package.json配置jest配置测试文件目录编写setup.js编写第一个测试文件jest.fn()和jest.spyOn()jest 解析scss失败测试vuex$refs定时器测试函数调用n次手动调用生命周期处理其他模块导入的函数测试插槽 前情
uniapp推荐了测试方案dcloudio/uni-automatordcloudio/uni-automator属于自动化测试api提供的示例偏重于渲染组件判断当前渲染的组件是否和预期一致vue推荐的测试方案vue test utils属于单元测试可以搭配jest、mocha等单测运行器
我选了方案2️
关于vue的组件测试vue官方提到 你的 Vue 应用中大部分内容都应该由组件测试来覆盖我们建议每个 Vue 组件都应有自己的组件测试文件。 当进行测试时请记住测试这个组件做了什么而不是测试它是怎么做到的 对于 视图 的测试根据输入 prop 和插槽断言渲染输出是否正确。 对于 交互 的测试断言渲染的更新是否正确或触发的事件是否正确地响应了用户输入事件 本身的测试写起来很简单就是挺多东西需要配置的比较麻烦记录在后文
安装依赖
vue/test-utils vue2项目安装 npm install --save-dev vue/test-utils1 不指定的话默认安装最新适合vue3项目吧jestvue-jest为了处理.vue文件 npm install --save-dev vue/vue2-jest29 最后写jest版本babel-jestjest-environment-jsdom
jest版本在27以上是要安装jest-environment-jsdom的 其他版本下如果报错
[vue-test-utils]: window is undefined, vue-test-utils needs to be run in a browser environment. You can run the tests in node using jsdom 可以尝试 npm install --save-dev jsdom jsdom-global
// 在测试的设置 / 入口中
require(jsdom-global)()package.json配置
加一条就好
scripts: {test: jest
},jest配置
可以配在package.json的jest选项中 也可以新建jest.config.js我选了后者
module.exports {moduleFileExtensions: [js,vue],transform: {^.\\.vue$: rootDir/node_modules/vue/vue2-jest,^.\\.js$: rootDir/node_modules/babel-jest},moduleNameMapper: { // webpack中设置了别名设置为/src 的别名就需要配这个^/(.*)$: rootDir/src/$1},testMatch: [**/__tests__/**/*.spec.js],transformIgnorePatterns: [rootDir/node_modules/],testEnvironment: jsdom // jest v27以上要加
}⚠️ 官网提到的一个注意点
如果你使用了 Babel 7 或更高版本
你需要在你的 devDependencies 里添加 babel-bridge
($ npm install --save-dev babel-core^7.0.0-bridge.0)。我在运行时有相关的报错提示所以我也按照这样安装了 如果你也有的话可以参考一下
测试文件目录
新建__tests__目录放在src目录下可以根目录下也可以 注意别少打s 目录下的测试文件扩展名应该是.spec.js或者test.js 我选了前者 这个也可以改想改去找jest文档————
编写setup.js
通常用于执行一些全局的设置或引入一些测试所需的全局依赖以确保这些设置和依赖在所有测试文件中都可用
jest.config.js中新加一条 setupFiles: [./__tests__/setup.js]
__test__文件夹下新建setup.js
1.项目中用到的uni或者wx的api是识别不了的所以放在这里预先配置一下 2.在Vue.prototype上挂载的比如$toast、$api$store、直接用this.调用的时候也是识别不了的也要在这配置一下
localVue可以理解成创建本地的vue实例可以使用localVue.prototype挂载一些东西而不会污染到真正的Vue.prototype我在这挂到全局了实际上可以在每个单独的测试文件中都create新的
import { createLocalVue } from vue/test-utils;
import Vuex from vuex
import axios from axiosconst CODE 用户登录凭证;
// 创建的一个 Vue 的本地拷贝
const localVue createLocalVue()localVue.use(Vuex)
const store new Vuex.Store({state: {},mutations: {login: jest.fn()}
})
localVue.prototype.$store store
localVue.prototype.$toast jest.fn()
// 后面很多场景的使用是const {confirm} await this.$modal(xxx), 这里直接模拟cofirm为true
localVue.prototype.$modal jest.fn(() Promise.resolve({ confirm: true }))
localVue.prototype.$api {student: {studentLogin: jest.spyOn(axios, post)},
}global.uni {showLoading: jest.fn(),hideLoading: jest.fn(),navigateTo: jest.fn(),switchTab: jest.fn(),getStorageSync: jest.fn(),setStorageSync: jest.fn(),login: jest.fn(() Promise.resolve([,CODE]))
}
global.setValue (target, value) {target.element.value valuetarget.trigger(input)
}
global.wx global.uni
global.localVue localVueps这里挂了一个全局的方法setValue因为官方的那个我使用会报错显示没有setValue()查看setValue()不知道是不是因为我的input是小程序的
编写第一个测试文件
对组件StudentLogin.vue新建studentLogin.spec.js
变更一个响应式属性之后为了断言这个变化测试需要等待 Vue 完成更新可以
await vm.nextTick() 2. await 操作比如trigger
import { shallowMount } from vue/test-utils;
import StudentLogin from /pages/student-login/student-loginconst TEST_VALUE 123456const TEST_TIP {NO_NUMBER: 请填写学号,NO_PASSWORD: 请填写密码}
// describe(name, fn): 表示一组测试如果没有describe那整个测试文件就是一个describe。name是这组测试的名字fn是这组测试要执行的函数。
describe(StudentLogin.vue, () {let wrapper;beforeEach(() {// shallowMount和mount区别在于不会挂载子组件比较适合单元测试子组件的测试逻辑单独写wrapper shallowMount(StudentLogin, {localVue})})// formSubmit触发时输入账号没输入密码提示请填写密码test(if formSubmit triggered with number but no password, show tip, async () {setValue(wrapper.find(input[namenumber]), TEST_VALUE)await wrapper.vm.$nextTick();await wrapper.find(.submit-btn).trigger(click)expect(localVue.prototype.$toast).toBeCalledWith(error, TEST_TIP.NO_PASSWORD)})// formSubmit调用后应该发起请求it(if formSubmit done, send request, async () {setValue(wrapper.find(input[namenumber]), TEST_VALUE)setValue(wrapper.find(input[namepassword]), TEST_VALUE)await wrapper.vm.formSubmit()expect(localVue.prototype.$api.student.studentLogin).toBeCalled();expect(localVue.prototype.$api.student.studentLogin).toBeCalledWith(TEST_VALUE, TEST_VALUE, CODE)})// 销毁所有被创建的 Wrapper 实例enableAutoDestroy(afterEach)
})jest.fn()和jest.spyOn()
承接上文 轻轻记录一下jest.fn()和jest.spyOn() 他们都是用来模拟函数的行为都会跟踪函数的调用和传参 区别jest.fn()是创建一个全新的模拟函数jest.spyOn()一般是模拟对象上的现有方法
比如 页面需要axios发请求但是我们测试的时候不需要实际调用 就可以利用
localVue.prototype.$api {student: {studentLogin: jest.spyOn(axios, post)},
}使用场景非常多后文也会涉及 他们两返回的其实就是mockFn在jest官网有非常多对mockFn的操作 指路mockFn
我常用的一个mockFn.mockResolvedValue(value) 例如 测试这个函数是否成功发送请求但我们无需发送真的请求就可以模拟返回值
async getList() {const { data } await this.$api.student.getData()this.list data
}// test.spec.js
test(, async () {localVue.prototype.$api.student.getData.mockResolvedValue({list: [1,2,3]})await wrapper.vm.getList()expect(wrapper.list.length).toBe(3)
})⚠️提醒一下自己注意 比如说我们要断言trigger某个操作或者更新了页面之后某个函数应该要被调用 会使用
const spy jest.spyOn(wrapper.vm, someFunction)
expect(spy).toBeCalled()但要注意这个必须要写在更新操作之前如果写在之后是会断言错误的 jest.spyOn写在了trigger之后也就是开始跟踪的时候已经触发完了 那么expect(infoSpy).toBeCalled()就会失败 test(if term picker triggered, async () {const picker wrapper.findComponent(picker)await picker.trigger(change, 1);const infoSpy jest.spyOn(wrapper.vm, getInfo)expect(wrapper.vm.termIndex).toBe(1)expect(infoSpy).toBeCalled()})jest 解析scss失败
比如这个页面有引入scss import { THEME_COLOR } from /uni.scss; 如果不做配置的话就会报错
解决方法 新建一个styleMock.js
// styleMock.js
module.exports {process() {return {code: module.exports {};,};},
};然后在jest.config.js中配置transform
transform: {^.\\.vue$: rootDir/node_modules/vue/vue2-jest,^.\\.js$: rootDir/node_modules/babel-jest,\\.(css|less|scss|sass)$: rootDir/styleMock.js,
},然后运行npm run test如果还是没生效可以试试关闭编辑器重新启动
测试vuex
这里不提官网有的部分有需要可自查 在组件中测试vuex
目前场景是这个组件在计算属性中使用了mapState
computed: {... mapState([flag, userInfo])
}然后当userInfo.level 1 flag 1时候要渲染某容器我需要测试这个那么就需要修改state中的数据 由于前面在setup.js中已经在localVue上安装了vuex这里就通过localVue来访问
localVue.prototype.$store.state.flag 1
localVue.prototype.$store.state.userInfo { level: 1 }不要用store.commit()不生效
⚠️更改完数据后要等待页面更新记得await nextTick()一下否则断言会失败
$refs
类似于这样的代码
close() { // 清除校验结果this.$refs.form.clearValidate();this.$emit(close);
},this.$refs.form.clearValidate();会报错提示找不到clearValidate这个function 解决方法1: 模拟一个form塞在stubs里
// 这里要写的是组件的名字不是ref设置的名字const UniForms {render: jest.fn(),methods: {validate: () {},clearValidate:() {}}}
wrapper shallowMount(ForgetPassword, {localVue,stubs: {UniForms}
})(模板上uni-forms refform/uni-forms) 但我这个例子用这个方法不太行会影响我别的测试一些元素渲染失败wrapper.find时会找不到 先记录在这吧
解决方法2: 加一行 wrapper.vm.$refs.form.clearValidate jest.fn()
如果有要返回的数据可以在jest.fn()中直接模拟 比如说 我们需要拿到返回的password、email简单的jest.fn()无法满足需求
const { password, email } await this.$refs.form.validate();设定jest.fn()模拟的函数返回成功值
wrapper.vm.$refs.form.validate jest.fn(() Promise.resolve({ password: 1, email: 1
}))后续 又有一处用到$refs
mounted() { this.$refs.form.setRules(this.formRules);
}这次是在mounted()里使用方法2就用不了了因为需要先mount(wrapper)才能拿到wrapper.vm但这里又是要在mounted中执行的假如我们使用wrapper.vm.$refs.form.setRules jest.fn()其实就已经晚了mounted已经执行完了
这个时候就可以用方法1~
定时器
检验有关定时器的方法
setTime(number) {this.codeText 倒计时${number}s;if(!number) {this.codeText 发送验证码;this.isSending false;this.timer null;return;} else {number--;}this.timer setTimeout(() {this.setTime(number);}, 1000);
},使用jest.useFakeTimers()指定全局使用假的定时器api jest.advanceTimersByTime(1000)模拟时间快进1s
jest.useFakeTimers()
const sendCodeBtn wrapper.findComponent(.send-code)
test(if setTime triggered 60, change btn content and start countdown, async () {const setTimeSpy jest.spyOn(wrapper.vm, setTime)await wrapper.vm.setTime(60)expect(sendCodeBtn.text()).toBe(倒计时60s)// 过一秒jest.advanceTimersByTime(1000)expect(setTimeSpy).toBeCalledWith(60 - 1)})test(if setTime triggered 0, change btn content and close timer, async () {await wrapper.vm.setTime(0)expect(sendCodeBtn.text()).toBe(发送验证码)// 过一秒jest.advanceTimersByTime(1000)expect(wrapper.vm.timer).toBe(null)})测试函数调用n次
本来想测 1.titleInput或contentInput无内容时 提示’请输入必要内容’ 2.titleInput和contentInput都有内容时 不显示提示 错误写法
test(, async () {await form.trigger(submit)expect(localVue.prototype.$toast).toHaveBeenCalledWith(none, 请输入必要内容)setValue(titleInput, TEST_VALUE)await form.trigger(submit)expect(localVue.prototype.$toast).toHaveBeenCalledWith(none, 请输入必要内容)setValue(contentInput, TEST_VALUE)await form.trigger(submit)expect(localVue.prototype.$toast).not.toHaveBeenCalled()
});但上面这种写法是错的实际上localVue.prototype.$toast的调用是累积的不是相互隔离的第三次expect(localVue.prototype.$toast)的时候实际上已经被调用三次了那么not.toHaveBeenCalled()就不可能通过测试
这时候应该使用toHaveBeenNthCalledWidth()第一个参数写n表示第n次 第三次的时候不应该被调用就用toHaveBeenCalledTimes()判断总调用次数
test(, async () {await form.trigger(submit)expect(localVue.prototype.$toast).toHaveBeenNthCalledWith(1, none, 请输入必要内容)setValue(titleInput, TEST_VALUE)await form.trigger(submit)expect(localVue.prototype.$toast).toHaveBeenNthCalledWith(2, none, 请输入必要内容)setValue(contentInput, TEST_VALUE)await form.trigger(submit)expect(localVue.prototype.$toast).not.toHaveBeenCalledTimes(3);
});手动调用生命周期
比如说 onLoad是小程序里的生命周期
onLoad({code}) {this.code code;// 每10min刷新一次if(!this.code) {this.getSignInCode();this.timer setInterval(() { this.getSignInCode() }, 600000);}
}这里想测试code为0的时候是否调用了函数getSignInCode且过了10min是否再次调用
我想手动调用onLoad()onLoad并不在wrapper.vm上不能通过wrapper.vm.onLoad访问 可以通过两种方式找到这个组件名叫ShowQRcode
ShowQRcode.onLoad({ code: 1 })wrapper.vm.$options.onLoad({ code: 1 })
但都会报错this.getSignInCode is not a function因为getSignInCode是在wrapper.vm上的所以这里要更改this指向 ShowQRcode.onLoad.call(wrapper.vm, {code: 0 }) test(, () {const signInSpy jest.spyOn(wrapper.vm, getSignInCode)ShowQRcode.onLoad.call(wrapper.vm, { code: 0 })expect(signInSpy).toHaveBeenCalledTimes(1)jest.advanceTimersByTime(600000)expect(signInSpy).toHaveBeenCalledTimes(2)})处理其他模块导入的函数
场景
import { uploadImg } from /util/uploadImg.js;async selectImg(res) {// 上传图片const { url } await uploadImg(res.tempFilePaths, files)this.imgPaths.push(url[0]);
}如果要测试selectImg()当执行到uploadImg()就会报错
我们就可以利用jest.mock来模拟这个模块 记录一下jest.mock的简单使用 官网的例子
// banana.js
export default () banana;// test.spec.js
// 后续的测试中任何导入./banana模块的代码将会被自动模拟而不是实际的banana.js模块
jest.mock(./banana);
// 这个导入的bannana就被自动模拟了
const banana require(./banana);
// 不会返回banana因为被模拟了默认返回undefined
banana(); // will return undefined 还可以接收一个函数显式指定模块导出的内容
// 相当于
// const mockFn jest.fn(() bannana
// export default mockFn
jest.mock(./bannana, () {return jest.fn(() bannana);
});const bannana require(./bannana);
bannana(); // Will return bannana;所以这里就这样写
// 相当于
// export const uploadImg jest.fn(() Promse.resolve({ data: TEST_UPLOAD_RESPONSE}))
jest.mock(/util/uploadImg.js, () ({otherFunction: xxx,uploadImg: jest.fn(() Promise.resolve({ data: TEST_UPLOAD_RESPONSE }))
}));测试插槽
项目比较简单用插槽的地方很少甚至没用到作用域插槽 这里只记录最简单的方法 官网是有例子的测试插槽
就是在shallowMount的时候配置slots beforeEach(() {wrapper shallowMount(Detail, {localVue,slots: {list: viewlist/view,operation: viewoperation/view}})})这里slots配置的就是模拟传入插槽的内容 比如list: viewlist/view就是该组件内有一个插槽出口slot namelist/slot 然后我们模拟传入这个插槽的内容是viewlist/view 之后打印wrapper.html()会发现插槽出口确实都被替换成了我们预设的内容 只需要断言expect(wrapper.html()).toContain(viewlist/view)即可完成测试
这里还出现一个问题我有一个插槽出口长这样
slot nametopview classtop__button v-ifflag xxxtext{{ xxx }}/text/view
/slot在插槽中指定了默认内容且默认内容要通过v-if控制显示隐藏 并且这个地方我也写了一个测试是测试top__button的显隐 如果我一开始预设的时候预设了插槽top的内容就会导致这个测试失败因为找不到top__button了直接被替换成了我预设的内容 其实失败的原因是我两个测试共用了一个wrapper的配置习惯写在beforeEach里
解决的方法就是在这个测试中单独的再重新创建一个wrapper不要预设slots就好
补充 测试作用域插槽
参考 https://juejin.cn/post/7119314584371986468?searchId2023092122585499D5137C15C4283D9452 https://blog.csdn.net/pk142536/article/details/122255192 https://zhuanlan.zhihu.com/p/457648810