前言
经过本系列教程的 1 、 2 两部分,我们已经可以跑起来一个集成测试了,但是有个致命的问题,就是接口的返回是通过 mock window.fetch 来实现的,相当于数据写死,这很不灵活,因为想要写出完美的测试,我们就需要模拟各种接口状态,各种动态数据,要求再高点甚至需要模拟鉴权,那这就不是简单的覆盖方法可以实现的。所以我们需要一些第三方依赖,下面介绍一下实现步骤。
step 1: 新增业务并进行 case design
为了让便于展示测试,我给网站加了一个新功能,在首页会展示网站的标题并且支持修改,这样就需要一个 get 接口,一个 post 接口,并且还需要数据库。于是我用 koa 搭了一个后端服务,提供两个接口,数据库我不熟,所以我另辟蹊径,在 notion 上新建了一个 page,通过 notionSDK 来获取和更新 page name,这样就成了我的数据库。别看我说的简单,其实这一套花了我一个下午,有各种意想不到的问题,比如 koa 如何解析 body、 notion 数据结构太复杂、eslint。 tsconfig 配置的问题等等,期间还错把notion key 给 push 了上去,已经给 disable 了。 具体怎么实现就不细讲了,这不是本系列教程的重点,代码都在github.com/alpacachen/... ,感兴趣可以自己去看一下 server 目录。
总之最后实现的需求大致长这个样子
对这个需求进行 case design,得到
xml
G: 网站加载完成
T: 会展示标题 "集成测试框架"
W: 点击修改按钮
T: 会弹出修改弹窗
W: 在输入框中输入 "集成测试框架 2",点击 "确认修改"
T: 弹窗会关闭,网站标题会更新为"集成测试框架 2"
step 2:完善 app-context,支持xhr
install http-request-mock
和lokijs
这两个依赖,重写一下 fake-http-handler.ts这个文件
ts
import HttpRequestMock from 'http-request-mock';// 引入 mock 请求的依赖
import { HupuPostDb, TitleDb } from './fake-db';// 引入提前准备好的 fakedb
const mocker = HttpRequestMock.setup()
// mock一下 hupu 接口,数据从写死改为从 db 中读
mocker.mock({
url: '/hupu/api/v2/bbs/walkingStreet/threads?page=1',
method: 'GET',
body: { data: { threadList: HupuPostDb.find() } },
});
// mock 获取标题的请求
mocker.mock({
url: '/api/title',
method: 'GET',
response: () => {
return TitleDb.findOne().title
}
});
//mo 修改标题的请求
mocker.mock({
url: '/api/title',
method: 'POST',
response(req: { body: { title: unknown; }; }) {
// 找到并更新
const prevTitle = TitleDb.findOne().title;
TitleDb.findAndUpdate({ title: prevTitle }, (d) => {
d.title = req.body.title
})
return {};
}
});
http-request-mock
这个依赖应该是国人开发,有优秀的中文文档,大家可以看一下,我就不赘述了github.com/huturen/htt...
新建文件 fake-db.ts
ts
import loki from 'lokijs'
import { FakeHupuData } from './fake-data'
const db = new loki('fake-db')
// 创建虎扑热帖集合
const HupuPostDb = db.addCollection('hupu-post')
// 插入默认数据
HupuPostDb.insert(FakeHupuData)
//创建 title 集合
const TitleDb = db.addCollection('title')
// 插入默认数据
TitleDb.insert({ title: '集成测试教程' })
export { HupuPostDb, TitleDb }
接下来我们只需要在 app-context.ts中引入一下 fake-http-handler.ts,这样我们的集成测试就支持了接口和数据的库查询修改。
step 3: 根据 case借助 ai 补充新的测试
接下来我们试试在测试跑一遍中调用接口获取数据,修改数据,再次获取最新数据这个流程。
ts
describe('W: 点击标题旁边的 修改按钮', () => {
beforeEach(() => {
fireEvent.click(screen.getByTestId('change-title-but'))
})
it('T: 会弹出弹窗,input 中展示 "集成测试教程"', () => {
expect(screen.getByTestId('title-modal')).toBeTruthy()
expect(within(screen.getByTestId('title-modal')).getByDisplayValue('集成测试教程')).toBeTruthy()
})
describe('W: 修改 title 为 "集成测试教程 2" 关闭弹窗', () => {
beforeEach(async () => {
const input = within(screen.getByTestId('title-modal')).getByDisplayValue('集成测试教程')
fireEvent.change(input, { target: { value: '集成测试教程 2' } })
fireEvent.click(screen.getByText('确定修改'))
await vi.advanceTimersToNextTimerAsync();
})
it('T: 弹窗关闭,顶部栏的文案变为"集成测试教程 2"', () => {
// header
expect(screen.queryByTestId('title-modal')).toBeFalsy()
expect(screen.getByTestId('header').innerHTML).toContain('集成测试教程 2')
})
})
})
这里需要强调一下,这个测试我是直接接在了之前的测试后面,很方便而且直观,只就是为什么我在第一章的时候强调写测试规范的问题,如果测试规范,是非常易于维护和新增的,会大大降低写测试的压力,从此形成良性循环
step: 4 见真章
直接看npm run test
npm run coverage
得到结果
可以看到除了 server 的代码还是一片绿,并且也没有加几行测试代码 🎉
大家不妨想一想,如果没有集成测试,依靠单测想要达到这种程度的覆盖率需要写多少代码,多少文件?至少需要 header.test.ts和 store.test.ts两个文件,还要 mock 数据,想想就觉得麻烦。
所以重要的事情说三遍,在前端领域
绝大多数单元测试没有任何卵用
绝大多数单元测试没有任何卵用
绝大多数单元测试没有任何卵用
请不要再写单元测试让你自己和你的同事难受了,集成测试才是王道。
结尾
我们集成测试框架基本完成,麻雀虽小,却五脏俱全。能看到这里的同学想必一定会有些收获,可以尝试拿着这一套思路去套用到你们当前的项目中,年终绩效就不用愁。毕竟一个能写好测试的前端还是不多见的。
当然我这些代码还是有很多值得优化的点,有一些 corner case 发现了但是没有补充测试。比如文件目录可以更加合理,app-context的代码可以组织的更好。但是作为一个入门教程我认为达到这个程度就够了,切勿直接套用到生产项目中,我提供的是思路,不是代码。
如果觉得我这三篇文章帮助到你们了,就帮忙点个赞吧!