前言
上文写了一个组件的单测,实现了 100%覆盖率,接下来就引出了另一个问题,如何覆盖所有的代码?每个组件写一个单测?每个都 mock 一遍?
这是所有尝试开始写测试的同学遇到的最头疼的问题,无穷无尽的 mock和样板代码。并且有的组件内部函数还很难被调用到,需要抽成 util 再写一个单测。
这个问题我之前也遇到了,解决的办法就是集成测试
集成测试 (英语:Integration testing),又称组装测试 ,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作
那么接下来我们就来实现一个集成测试的架子。 其实 react应用就是由无数个小组件构成的大组件,那么我们直接在测试里面 render 整个 App 不就行了吗?非常简单
step 1: 在测试目录新建文件 app-context.tsx
这个文件的作用就是统一测试架构的上下文,并且能够统一管理。
我就直接贴代码了,通过注释来介绍每一行的作用。
由于我们的测试项目非常简单,所以这个上下文也非常简单。真实项目的上下文肯定是这个代码量的几十几百倍,并且构造起来也是很困难的。
ts
// app-context.tsx
import { MemoryRouter } from 'react-router-dom' // react-router 在测试中需要使用 MemoryRouter 来替换 BrowserRouter
import APP from '../App' // 应用入口
import { render, cleanup, within } from '@testing-library/react' // react 测试依赖
import { vi } from 'vitest' // vitest 的工具函数集,提供了常用的方法,比如 mock spy timer 等
import { fakeHttpHandler } from './fake-http-handler' // mock 接口的返回数据
vi.useFakeTimers() // 代理定时器相关
window.fetch = fakeHttpHandler // 重写 window.fetch
export const createAppContext = () => {
// 整个应用
const container = render(<MemoryRouter>
<APP />
</MemoryRouter>)
// 抽一些常用的方法
const inSideBar = () => {
return within(container.getByTestId('side-bar'))
}
return {
container,
inSideBar,
cleanup,
}
}
// 导出 appContext 的类型
export type AppContext = ReturnType<typeof createAppContext>
ts
// fake-http-handler.ts
// 重写一个 fetch 方法,让它能能够写死返回需要的数据
import { fakeResponse } from "./fake-response"
export const fakeHttpHandler = async (...args: Parameters<typeof fetch>) => {
console.log(args[0])
const res = {
json: async () => {
return { data: fakeResponse[args[0] as string] }
},
} as Awaited<ReturnType<typeof fetch>>
return res
}
// fake-response.ts
// 我直接把当时接口的返回值 copy 下来。
export const fakeResponse: Record<string, object> = {
'/hupu/api/v2/bbs/walkingStreet/threads?page=1':{
threadList: [data1,data2,...]
}
}
写完这几个文件后,最简单版集成测试的的框架已经搭好了,接下来就可以愉快的写测试了。
step2: 写集成测试
通过我们封装的这个上下文,创建一个测试就非常简单了,大致的样板代码为
ts
import { afterEach, beforeEach, vi, describe, expect, it } from "vitest";
import { AppContext, createAppContext } from "../app-context";
describe('G: 加载应用', () => {
let _: AppContext
beforeEach(() => {
_ = createAppContext()
})
afterEach(() => {
_.cleanup()
})
describe('W: 数据加载中', () => {
it('T: 会展示 loading', () => {})
describe('W: 数据加载完成', () => {
it('T: 会展示文件列表', () => {})
})
})
})
然后我们对整个应用做 GWT case design,做完之后开始写测试代码,现在借助 ai 功能,写测试代码是非常快的,如果你的测试框架搭建的足够完善,ai 生成的代码准确度就越高,我们只需要做一些小修改即可,比如如何等待异步事件结束,如何模拟用户事件等。此处不再赘述,代码在这里,一共 50 行。 github.com/alpacachen/...
这两步结束后我们就有了第一个集成测试的 test case。跑一下测试覆盖率看看
效果立竿见影,我们之前一个组件的单测就 77 行,现在有了集成测试,50 几行代码就可以实现几乎全覆盖。 并且看一下我们的测试报告 npm run test
是不是一目了然,清晰直观,这就是为什么我说,好的测试 case 胜过一切项目文档。
总结
这一篇文章介绍了搭建集成测试的流程,这其实没有任何难度,难得是想到这个思路。同时我展示了一份好的测试用例 demo,希望能帮助到大家。
一些感想
我是今天开始写文章才认真看了看掘金的推荐流,只能说前端圈还需努力,希望大家少一些浮躁,多一些沉淀。比如写测试这件事非常锻炼人心智,浮躁的人是没有办法静下心来找出 corner case 并且写补全测试的。因为写测试是对未来的投资,一份测试代码,刚写出来的时候是一文不值的,他的价值会随着时间的增加而增加,如果你写的测试测试代码,五年后还在守护着系统稳定运行,那它就是"价值连城"的。