企鹅极客丨产品高质量交付利器之前端自动化测试(下)

在前两天发布的《企鹅极客丨产品高质量交付利器之前端自动化测试(上)》一文中,我们对单元测试已经有了深刻的认识,单元测试可以帮助我们更加有效地检测代码中的问题,减少Bug数量并提高开发效率。
而今天的内容我们将逐步介绍各种测试场景下的测试方法和技巧,并说明如何使用常用的断言库来编写测试代码。同时还会介绍如何生成测试报告,并对总体的测试流程进行总结,以帮助您更好地理解和应用单元测试。
模块测试
这里的模块而言主要针对的是页面,也就是常说的router部分,这部分主要包括组件的自定义事件以及整体UI功能正确性验证。
页面结构测试
通过mount、shallowMount、find、findAll方法都可以返回一个包裹器对象,包裹器会暴露很多封装、遍历和查询其内部的Vue组件实例的便捷的方法。
find和findAll方法都可以都接受一个选择器作为参数,find方法返回匹配选择器的DOM节点或Vue组件的Wrapper,findAll方法返回所有匹配选择器的DOM节点或Vue组件的Wrappers的WrapperArray。
情景设想:
在Home下有一个p标签,class为.outer,里面的内容为Aaron。
import Home from "@/pages/Home.vue";
import { mount } from "@vue/test-utils";
describe('Test for Home Page', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(Home);
});
it('get tag dom', () => {
// 使用Vue组件选择器
expect(wrapper.is(Test1)).toBe(true);
// 使用CSS选择器
expect(wrapper.is('.outer')).toBe(true);
// 使用CSS选择器
expect(wrapper.contains('p')).toBe(true)
// 内容是否正确
const contentText = wrapper.find('p').text();
expect(contentText).toBe("Aaron")
});
it('has tag', () = > {
// isEmpty():断言 Wrapper 并不包含子节点。
expect(wrapper.find("button").isEmpty()).toBeFalsy();
// exists():断言 Wrapper 或 WrapperArray 是否存在。
expect(wrapper.findAll('img').exists()).toBeFalsy()
});
it('has className', () = > {
// attributes():返回 Wrapper DOM 节点的特性对象
expect(wrapper.find('p').attributes().class).toContain('outer');
// classes():返回 Wrapper DOM 节点的 class 组成的数组
expect(wrapper.find('p').classes()).toContain('outer');
});
it('has style', () = > {
// hasStyle:判断是否有对应的内联样式
expect(wrapper.find("p").hasStyle('padding-top', '10')).toBeTruthy()
});
});路由测试
路由是基于浏览器环境而言的,在单元测试中是没有办法正常使用路由的,需要通过jest.mock方法模拟出vue-router环境。
jest.mock("vue-router", () => {
const realModule = jest.requireActual("vue-router");
const mockRouter = {
...realModule,
currentRoute: {},
useRouter: () => ({
push(route: any) {
mockRouter.currentRoute.value = route;
},
currentRoute: mockRouter.currentRoute
})
};
return mockRouter;
});在上述代码中,通过函数所返回的对象,模拟了一个vue-router的环境,并当环境触发push操作的时候更改了currentRoute的值。
情景设想:
假设现在有两个路由配置,分别是Home和About,在Home页面中有一个按钮,点击之后跳转到About页面。
现在基于现有情景去写单元测试,具体代码如下:
import Home from "@/pages/Home.vue";
import { useRouter } from 'vue-router';
test("Home.vue GoAbout", async () => {
const wrapper = mount(Home as any);
const oBtn1 = wrapper.find("#btn1");
await oBtn1.trigger("click");
const router = useRouter();
const routerName = router.currentRoute?.value?.name;
expect(routerName).toBe("About");
});这里需要注意,在代码中调用push时所传递的参数会影响到最终的断言结果。如上代码,在正式环境代码中调用push时所传入的是push({ name: "About" }),所以在最终断言时使用About为最终需要断言的结果。
状态管理测试
状态管理工具有很多,本文中所使用的状态管理工具是pinia,需要对其进行安装。
# npm
npm install pinia -S
# yarn
yarn add pinia -Spinia已经内置了单元测试的mock环境,我们只需要简单的配置一下即可。
import { setActivePinia, createPinia } from 'pinia';
beforeEach(() => {
setActivePinia(createPinia());
});情景设想:
在Home下有一个button点击之后需要更改pinia中的一个值(fooValue),更改后的值为张三。
现在基于现有情景去写单元测试,具体代码如下:
import Home from "@/pages/Home.vue";
import { useHomeStore } from "@/store/module/Home";
test("Home.vue Change Pinia", () => {
const homeStore = useHomeStore();
const vm = mount(Home as any);
const oBtn = vm.find("#btn");
oBtn?.trigger("click");
expect(homeStore.fooValue).toBe("Aaron");
});注意这里需要把对应的store文件引入进来,才可以读取到更改后的值。
数据请求测试
在开发中更多的需求是当点击某一个按钮时去触发一个请求或者去改变一个值。这种情况通常也是通过模拟的方式解决。
情景设想:
在Home下有一个button点击之后需要进行一个请求ajax或者一个异步操作,去验证最后所得到的值是不是所需要的。
现在基于现有情景去写单元测试,具体代码如下:
import Home from "@/pages/Home.vue";
it('Home onGetAjax', (done) => {
const wrapper = mount(Home as any, {
setup() {
const data = ref({});
const onGetAjax = async () => {
data.value = mockData.data;
}
return { onGetAjax, data }
}
});
wrapper.find('#btn3').trigger('click');
wrapper.vm.$nextTick(async () => {
await wrapper.vm.onGetAjax();
expect(wrapper.vm.data).toEqual(mockData.data)
done();
});
});上述onGetAjax中要模拟所有的数据处理操作,当然这里也可以使用axios进行真实的数据请求。除了这种方式还有另外一种方式处理。
import Home from "@/pages/Home.vue";
// quantityBaseList 最终的mock数据
import { quantityBaseList } from "./quantityBase.mock";
const ajaxMock = (vai) => vai();
const getData = () => {
return new Promise((res) => {
res(quantityBaseList)
})
};
jest.mock("axios", () => {
const mAxiosInstance = {
get: jest.fn()
};
return {
create: jest.fn(() => mAxiosInstance),
};
});
// 这里必须要写
// 需要在这里mock一下请求数据的函数
jest.mock('@/api/Home', () => {
return {
getCreateCustomerData: jest.fn(getData)
}
});
it("Home getTabeList", () => {
return wrapper.vm.$nextTick(async (res) => {
const result = await ajaxMock(getData);
expect(result.data.items).toEqual(quantityBaseList.data.items);
expect(result.data.totalCount).toEqual(quantityBaseList.data.totalCount);
});
});上述代码直接mock了请求数据的方法,当数据请求完成之后所需要的后续操作进行断言也是可以的。
组件测试
单元测试环境配置成功,接下来也就是重中之重的环节对组件进行测试。单元测试由于是跑在node环境中的,所以很多情况不能直接去使用真实开发环境中的内容测试,所以很多情况需要在做单元测试手动模拟。
组件参数测试
上面说了很多关于页面相关的,除了页面之外也会对组件进行相关的测试。关于组件的话最常见的可能就是props参数。
情景设想:
一个组件需要接收一个prop是model<object>,只需要测试一下这个model是否可以正常接收。
let model = ref({
name: "张三"
});
it("provide/inject", () => {
let parent = mount(Detail as any, {
props: { model },
});
expect(child.vm.parentProps?.model).toEqual(model);
});自定义事件测试
自定义事件一般指的是,当组件内部执行完某一事件之后,需要通知上一层做出一些响应操作,系统开发中会经常用到。
情景设想:
在Home下有一个Foo的组件,Foo中有一个按钮当这个按钮点击的时候Home需要执行一个函数。
import Home from "@/pages/Home.vue";
import Foo from "@/components/Foo/index.vue";
describe('Test for Foo Component', () => {
wrapper = mount(Home as any);
it('addCounter Fn should be called', () = > {
const mockFn = jest.fn();
wrapper.setMethods({
'addCounter': mockFn
});
wrapper.find(Foo).vm.$emit('add', 100);
expect(mockFn).toHaveBeenCalledTimes(1);
});
wrapper.destroy()
});这里使用了$on方法,将Home自定义的add事件替换为Mock函数,对于自定义事件,不能使用trigger方法触发,因为trigger只是用DOM事件。自定义事件使用$emit触发,前提是通过find找到Foo组件。
计算属性测试
计算属性是一个数据, 依赖另外一些数据计算而来的结果,当一个变量的值,需要用另外变量计算而得来。对于计算属性的应用场景也是蛮多的。
情景设想:
在FooText组件中,有一个input标签,通过计算属性把input输入的值进行反转,并输出到了组件中一个p标签中。
import FooText from "@/components/FooText/index.vue";
describe('Test for Foo Component', () => {
beforeEach(() => {
wrapper = shallow(FooText);
});
afterEach(() => {
wrapper.destroy()
});
it('test computed', () => {
// 可以通过 setProps 设置props属性
wrapper.setProps({needReverse: false});
wrapper.vm.inputValue = 'ok';
expect(wrapper.vm.outputValue).toBe('ok');
});
});数据监听测试
当一个值发生变化时需要执行一些其他操作,一般用于组件封装时,当外部数据发生变化之后,组件内部需要对组件状态进行调整。
情景设想:
在FooText组件中,通过watch监听input的value的值变化后需要执行一个函数。
import FooText from "@/components/FooText/index.vue";
describe('Test watch', () = > {
let spy;
beforeEach(() = > {
wrapper = shallow(FooText);
spy = jest.spyOn(console, 'log');
});
afterEach(() = > {
wrapper.destroy();
spy.mockClear()
});
it('is called with the new value in other cases', () = > {
// 对inputValue赋值时spy会被执行一次,所以需要清除spy的状态
// 清除已发生的状态
spy.mockClear();
wrapper.vm.inputValue = 'ok';
return wrapper.vm.$nextTick(() = > {
expect(spy).not.toBeCalled();
});
});
};依赖注入测试
项目中会用到provide/inject这种依赖注入的情况,这种情况一般会涉及到父子组件嵌套的情况。
情景设想:
两个组件分别是Detail和DetailItem,Detail组件中通过provide向下传入了model,需要测试DetailItem通过inject接收到的内容是否是父组件传入的。
import Detail from "@/components/Detail/index.vue";
import DetailItem from "@/components/DetailItem/index.vue";
it("provide/inject", () => {
let parent = mount(Detail, {
props: { model },
slots: {
default: DetailItem
}
});
let child = parent.findComponent(DetailItem);
expect(child.vm.parentProps?.model).toEqual(model);
});插槽测试
对于slot的测试也是必不可少的,特别有的时候slot所暴露出来的参数是否正确,也是让人关系的。
情景设想:
DetailItem组件可以使用slot对内容进行渲染,slot暴露出来了value参数以供渲染使用,测试value是否正确。
it("render slots", () => {
const wrapper = mount(DetailItem, {
global: {
provide: {
"detail": reactive({
model,
align: "left",
labelWidth: "100px"
})
},
},
slots: {
default: (item) => `<b>${item.value}</b>`
},
props: {
label,
prop,
align: "left",
labelWidth: "100px"
}
});
expect(`<b>${model.value[prop]}</b>`).toBe(wrapper.find(".w-full").text())
})测试报告说明与使用
sonarQube是一款代码质量管理工具,可以用于检查代码质量、安全性和可维护性等方面。它支持多种编程语言和技术栈,并提供了丰富的指标和报告,帮助开发团队更好地管理和优化代码质量。
对于前端项目,sonarQube可以使用前端的测试报告来评估代码质量。首先,需要在前端项目中配置好测试框架,并生成测试报告。将测试报告导入到sonarQube中,以便sonarQube可以读取并分析它们。sonarQube支持多种测试报告格式,例如JUnit、Surefire、Cobertura等。可以根据前端测试框架生成的测试报告格式,选择相应的sonarQube测试报告插件进行导入。
在sonarQube中配置好前端项目的检查项和指标,例如代码覆盖率、代码复杂度、代码重复率等。sonarQube会根据测试报告和检查项,生成相应的代码质量报告和指标分析。开发团队可以根据这些报告和指标,优化和管理前端代码质量。
当执行完单元测试之后会在项目根目录下生成一个名为coverage的文件夹,文件夹中clover.xml和lcov.info存放的即是覆盖率相关的内容。
sonar-project.properties的运行命令中添加:
// xml的位置
sonar.testExecutionReportPaths=test/covrage/test-report.xml
// lcov.info的位置
sonar.javascript.lcov.reportPaths=test/covrage/lcov.infojest执行完会生成一个覆盖率统计表,所有在覆盖率统计文件夹下的文件都会被检测,覆盖率指标:
File:文件路径Statements: 语句覆盖率,执行到每个语句Branches: 分支覆盖率,执行到每个if代码块Functions: 函数覆盖率,调用到程式中的每一个函数Lines: 行覆盖率, 执行到程序中的每一行
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 63.95 | 56.42 | 70.31 | 67.68 |
src/components | 68.84 | 65.11 | 67.85 | 73.8 |
Foo.vue | 67.91 | 65.11 | 66.66 | 73.17 |
------------------|---------|----------|---------|---------|-------------------总结
单元测试是提高软件开发质量的重要手段。它有助于企业提高产品质量和客户满意度,从而为企业的长期发展奠定坚实的基础。通过单元测试,企业可以及时发现并解决代码中的问题,提高代码的可维护性和可扩展性,降低代码维护成本和风险,并为未来的开发工作提供更好的基础。同时,单元测试还可以提高开发人员的信心和积极性,促进团队的合作和创新,为企业创造更多的价值和竞争优势。
在进行单元测试时,需要注意的是单元测试并不是一项简单的工作,需要测试者具备一定的技术和经验。测试者应充分考虑测试用例的覆盖率和测试结果的准确性,确保测试的有效性和可靠性。此外,测试者还需了解项目需求和功能,根据实际情况进行测试设计和测试用例编写,从而保证测试的全面性和完整性。
综上所述,单元测试是软件开发过程中不可或缺的一环。它可以帮助企业提高软件开发质量和客户满意度,为企业的长期发展奠定坚实的基础。因此,企业应高度重视单元测试工作,积极推广单元测试理念,不断提升测试水平和技术能力,为企业的未来发展注入新的动力和活力。

