本文作者为 360 数据平台部前端开发工程师
为什么要用AI写单测
这个问题分两点来说,第一是为什么要写单测,第二是为什么要用AI。
先来说为什么写单测,这很简单,我们项目或者说我们前端团队有公共模块,包含有组件类型、工具函数类型、hooks类型等,我们最大也是重点的项目,使用的技术栈是 React,因此这里说的 hooks 是指 react hooks,随着前端队伍壮大,开始考虑用开源的思路来维护这些代码,那么单测就是正规化的其中很重要一环。
至于第二点,主要是考虑提效,所以做一次尝试,项目里有祖传的 Jest 配置,但是具体需要什么依赖,需要根据所写的单测是什么类型,以及当前技术栈是什么版本,这就有一定搭建的学习成本,因此,本次设定为从0到1搭建 Jest 环境到跑通 AI 所写的用例。
使用的IDE
Cursor,版本0.2.5:
AI模型:
不是GPT-4
想要一直使用中文对话,可以这样配置:
点右上角的图标,打开辅助侧栏,点 MORE,然后告诉他,一直使用中文回答,这个配置目前只能有一条。
例子
Github仓库地址:
https://github.com/bdlite/hooks/tree/main
用来做例子的分支:
preview/previous
感兴趣的话可以一边 clone 下来,用 Cursor 打开文件夹,然后一边跟着操作。
开始
仓库都有什么

有从我们实际项目里剥离出的工程类配置文件,例如 .babelIrc、.gitignore、jest.config.js,配置项也只留取需要的
package.json 是使用 npm init 初始化的,生成过程略,涉及到要添加的包后文也会有提及
src 目录就是放源码了,es 目录是编译为 esModules 的制品路径,发布到 npm 用的是这个目录,因此单测也引用的这个目录
coverage 目录是 jest 自动生成的报告结果
__tests__ 目录是空的
看下祖传的jest.config.js
全选整段文件内容,会出现两个选项(这里的 Cursor 界面还有点小小的 bug),一个是 Edit,一个是 Chat,不同的是,Edit 可以直接帮你生成新代码或者改代码,Chat 是基于这段代码你可以问些问题,但不在编辑框内生成代码。

我这里想让它直接帮我生成注释,便于辅助分析一下这个配置有哪些不妥(懒得看官方文档的这里集合


生成的是英文,先 Reject,看来辅助侧栏里面配置的在代码编辑框里并不共用,重新调整 Prompt 输入:

这一次 Accept:

这么读起来,虽然是祖传的配置,似乎看不出什么毛病,当然为了制作例子,其实是有所调整的,至少项目里的路径配的没问题,因此这里的配置也是模拟了一下,路径也没问题,基于此,我们开始让Cursor写用例,然后跑起来看看。
看下源码

为了保证环境搭建及后续发布流程比较靠谱,我们先 check 一下哪些是 dependencies,哪些是 devDependencies 和 peerDependencies:
'react' 显然不能跟着打包,放 peer
'query-string' 这种库还是要严谨点,放 depend
去 package.json 确认一下是否放对了

这里有个问题,没有指定 react ,因此我让“助理”帮我加一下:


accept 之后多了个花括号,手动删吧,谁让它还是个孩子啊。
好戏开场

我这是当前打开 package.json 的情况下在侧边栏问的,再试试打开 useSearch.js 的情况下问问:

果然当前打开的文件就是提问的上文,可以见到上一个其实并不是我源码实现的内容,我估计是从其他地方找来的答案,果然是一本正经也能胡说八道呀
行,这里的方案给得还是蛮全面的,比方说教你加哪些依赖,手把手教你创建文件,可惜的是,它其实并不能读取整个工程,跨文件去理解上下文,因此它不知道的是,我配了 jest 去读 __tests__ 下的文件
review 下用例
依上文,用例只有三个,只看代码不执行的话,似乎做到了最小的功能检验
再换个思路试试

从以上两个提问的答案来看,看懂代码是没问题的,也能指出里面有问题的点,表现不错


基本上符合源码涉及的几个场景,不过,根据我的判断,似乎还有点问题:
没有开场处的验证 hooks 返回类型的用例
image.png 此处的第二个用例:如果 key 不是字符串或者 value 是 null,那么函数不会进行任何操作
看到这个才发现,源码里存在着 bug,为什么在问它有没有 bug 的时候没反应过来,因为当时还没做例子的时候问过,回答是没什么问题,看来这孩子也是变聪明了,也可能是当时的上下文有差异,导致孩子只顾夸,没理性思考,是的,我会去问写得好不好,哈哈
bug 就是,其实我们是期望 value 是 null 或者 undefined,那么会清掉 search 中对应的 key
其他问题不大,最多想起来什么场景补充一下就好
run 一下看
按上文提到的,在 __tests__ 目录下创建 useSearch.test.js 文件,然后把刚才的代码复制进去,但是引用 useSearch 函数的路径稍微改一下:
import { useSearch } from 'es/useSearch';
安装 jest 各种依赖

根据提示,最省事儿的办法,重新安装,并在命令后加上 --legacy-peer-deps
npm install --save-dev jest @babel/core @babel/preset-env @babel/preset-react babel-jest identity-obj-proxy react-test-renderer --legacy-peer-deps
执行 test 命令
该命令的配置同样是祖传的

在终端里执行:
npm run test
还得装依赖

OK~装它!

一步一脚印啊,继续装它!
再执行一次,这次是 Module ts-jest in the transform option was not found.
ts-jest 装完了,然后再 run test

做个例子不容易,那就继续装!
但是这里我会都装到 devDependencies,再 run

打开推荐的链接

OK,试一下 use react-test-renderer

装 react,装到 devDependencies,再 run

执行成功,祖传的命令写得倒没啥问题,上“链接”:
npm install --save-dev cross-env jest-environment-jsdom ts-jest react-dom react-test-renderer react
分析用例没通过的原因

选中这个用例,问下“助理”

点 Edit


好吧,怪我给的自由太过火,重新调整下


accept 然后跑看看,通过了,不截图了,看下一个

这个问题原因一致,但其实,这个不符合我们对这个 hooks 的期待,用错误的源码逻辑来生成错误的用例了属于是,直接改



why?
看下提示中的 Received 就知道了
第三个用例因为源码确实存在这个 bug,这目前来说是按照“预期” failed了
第四个为什么就不对了,第一次能跑出结果的时候不是通过了么,其实跟第二个用例的问题一样,不严谨导致的,这里面每一个用例中的 window 并不处于块级作用域,像第二个一样改过来就好了
让它改第四个


就剩下第三个用例没跑通了
改源码中的 bug
改源码,选中源码,然后 Edit


我哭死,感觉它不会写代码,又或者是我的锅?
罢了罢了,先手动改改
import { useCallback } from 'react'
import queryString from 'query-string'
export function useSearch() {
const searchList = [] // 同一组件连续调用的缓冲区
const getSearch = useCallback(() => queryString.parse(window.location.search), [ window.location.search ])
const setSearch = useCallback((key, value = null) => {
const search = getSearch()
if (search[key] === `${value}` || typeof key !=='string') return
searchList.push({ [key]: value })
const nextSearchData = { ...search, ...searchList.reduce((before, current) => ({ ...before, ...current }), {}) }
const nextSearch = queryString.stringify(nextSearchData, { skipNull: true })
window.history.replaceState(queryString.parse(nextSearch), '', `?${nextSearch}`)
}, [ getSearch, window.history ])
return { getSearch, setSearch }
}
这里改的是 src 目录下的文件,我们测的时候引用的是 es 的文件,因此在 package.json 的 script 里改下

重新执行

4个用例终于跑通了,yes!
利用 Prompt 提供上下文修复报错
再把之前提到的类型验证加上
it('should return an object with getSearch and setSearch functions', () => {
const { getSearch, setSearch } = useSearch();
expect(typeof getSearch).toBe('function');
expect(typeof setSearch).toBe('function');
});
run 后报错:

用侧边栏的 Chat 求救一下

选中用例,Edit



喜提大结局,撒花~
总结
本次实验操作路径
喂源码
生成用例
根据提示搭环境
review 用例和源码
找出问题并修复
丰富用例
遇到报错
喂错误信息
根据信息修复
跑通
如果没有喂给比较合适的上下文,可能会得不到准确的答案。
如果给的描述不够精准,例如我让它修复源码的 bug 就不尽如人意,相信给出足够多,足够精准的信息,应该还是可以的,或许你代码的结构上原本就有点问题,AI 不见得能够懂你希望连结构上的问题一起都能优化的心思,限于本文不是生成代码,而是用例,这一块没有展开来做尝试。
工程领域的期望
这个例子搭建 Jest 的过程还是比较顺利的,我在我们的业务项目里搭建,错误信息很难解,例子中的步骤其实是带有一点上帝视角的,包括里面其实已经自带了配置文件。
目前发现 AI 并不能阅读整个工程的配置、某个指定目录下的文件,据我了解,必须把整个工程丢给某个 AI 的程序,才能实现一些特定的任务,这对于追求轻量 IDE 的我们来讲,还远远不够,因此,我还是对于这一点抱有很高的期待,这对再次降低前端门槛将是一个很大的贡献。
描述问题的能力
这虽然不是本文想要提及的主题,大家可以通过例子自己去感受一下。
谢谢你读到了这里~
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。