兩個(gè) Prompt 實(shí)現(xiàn) Cursor 批量生成單測
最近用 Cursor 生成了一批單測代碼,合計(jì) 1.1w 行代碼,最開始生成的效果并不好,經(jīng)過一番調(diào)教,目前我所維護(hù)的倉庫已經(jīng)實(shí)現(xiàn)某種半自動(dòng)化的 UT 生成能力:在我并不理解代碼邏輯的情況下,僅需一行 Prompt ,即可為 Monorepo 中某個(gè) Package 所有源碼批量生成單測的效果:
圖片
圖片
這里面 Cursor 逐次做了幾件事情:
- 根據(jù)包名找到源碼目錄;
- 掃描源碼目錄,確定需要生成單測的源碼文件,并記錄到 .cursor/task.md 文件中;
圖片
- 針對 .cursor/task.md 標(biāo)記的每一個(gè)任務(wù),根據(jù) .cursor/rules/instruct-ut.mdc 指令迭代生成單測代碼,直至測試通過且覆蓋率達(dá)標(biāo)。
這是怎么做到的呢?PE!純粹的PE,沒有增加任何一行代碼!
Prompt 解析
首先,Cursor 支持多文件編輯,支持倉庫索引,支持配置 .cursorrules 等特性,因此其本身就有能力批量完成編程任務(wù),但在過往的測試中,批量生成單測的效果并不好,遇到過不少問題,主要可歸結(jié)為兩個(gè)點(diǎn):
- Package 的文件可能很多,Cursor Composer 經(jīng)常生成一部分文件后就會(huì)退出生成過程,只能部分完成任務(wù);
- 我所維護(hù)的是一個(gè) Monorepo 倉庫,體積很大,合計(jì) 300+ Package,200w 行代碼,代碼的嵌套層次很深,模塊依賴關(guān)系非常復(fù)雜,并且大量使用 TS Alias 等特性,導(dǎo)致單測的復(fù)雜度也相應(yīng)增加了許多;
針對第二點(diǎn),實(shí)測只需補(bǔ)充若干 Prompt ,用于明確單測代碼規(guī)范、技術(shù)棧等關(guān)鍵上下文信息即可(.cursor/rules/spec-for-ut.mdc):
# Unit Test Specification Expert
## Skills
1. Writing unit tests for React components and functions using Vitest.
2. Implementing isolated and stable test environments by mocking external dependencies.
3. Structuring test code following the arrange-act-assert pattern.
4. Ensuring compliance with file placement conventions for test files.
5. Avoiding source code modifications solely for unit testing purposes.
## Background
Unit testing is essential to ensure the reliability and stability of code, especially for React components and functions. Adhering to best practices and ensuring high test coverage is critical to maintaining a robust codebase.
## Goals
1. Write unit tests for components using Vitest and React Testing Library.
2. Implement integration tests for critical user flows.
3. Maintain unit test coverage of at least 80%.
4. Ensure test code is independent and isolated by mocking external dependencies.
5. Follow test file placement conventions to organize code effectively.
6. Avoid modifying source code for testing purposes.
## Rules
1. Use Vitest exclusively for unit testing; avoid Jest.
2. Use snapshot testing judiciously and only where appropriate.
3. Ensure test files are placed in the correct `__tests__` directory structure:
- Example 1: The unit test file for `root/packages/community/pages/src/bot/components/bot-store-chat-area-provider/index.tsx` should be placed in `root/packages/community/pages/__tests__/bot/components/bot-store-chat-area-provider/index.test.tsx`.
- Example 2: The unit test file for `root/xxx/src/foo/bar/xxx.ts` should be placed in `root/xxx/__tests__/foo/bar/xxx.test.tsx`.
4. Follow the arrange-act-assert pattern in test code organization.
5. Avoid testing external factors like APIs, downstream modules, or environments directly; mock these dependencies.
6. Use `expect(ele?.className).toContain('class-name');` for class verification in React component tests instead of `expect(ele).toHaveClass('class-name');`.
7. Maintain English usage throughout - avoid Chinese comments, test case names, etc.
# Reference
If necessary, you can refer to the following documents:
- [Vitest](https://vitest.dev/guide/)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [Arrange-Act-Assert](https://github.com/civic/arrange-act-assert)
難點(diǎn)在于第一點(diǎn):如何持續(xù)性地生成代碼?這里取了個(gè)巧,讓 Cursor 在將任務(wù)進(jìn)度記錄到外部文件中(.cursor/rules/instruct-ut.mdc):
# Instructions
When a user requests to generate unit tests for a directory of packages, first check the task progress in the `.cursor/unit-test` directory and prioritize resuming the previous progress. If no previous progress is recorded, start a new task and execute:
1. Find the code location corresponding to the package name;
2. Locate the source files containing logic code in the directory, record them as a task list, and save to `.cursor/unit-test/tasks.md`, for example:
- [ ] `src/index.ts`
- [ ] `src/utils/index.ts`
- [ ] `src/hooks/index.ts`
- [ ] `src/components/index.ts`
- [ ] `src/pages/index.ts`
- [ ] `src/services/index.ts`
- [ ] `src/types/index.ts`
3. Following the instructions in `./.cursor/spec-for-ut.md`, generate unit tests for each file from the previous step. After generation and passing tests, record in `.cursor/unit-test/tasks.md`, for example:
- [x] `src/index.ts`
- [x] `src/utils/index.ts`
- [x] `src/hooks/index.ts`
- [x] `src/components/index.ts`
- [x] `src/pages/index.ts`
- [x] `src/services/index.ts`
- [x] `src/types/index.ts`
4. If some unit test files consistently fail and only have one or two failing test cases, remove those test cases to ensure overall test passing
5. If any error cases are encountered during generation, record them in `.cursor/lessons` to avoid repeating the same mistakes in the future;
6. Recursively generate unit tests for each file until all files are completed, then exit the execution process
7. after all the files are completed, you should update the `.cursor/unit-test/tasks.md` file to reflect the progress.
# Limitations
- you can just use vitest to generate the unit test code.
之后,Cursor 每次生成單測時(shí)都會(huì)先到 tasks.md 文件中查找任務(wù)列表,恢復(fù)上次執(zhí)行進(jìn)度。
效果
在上述 Cursor Rules 基礎(chǔ)上,后續(xù)的交互就簡單許多了,只需使用下述 Prompt 即可:
為 @coze/api 包生成單測
//or
為 xxx 目錄/文件生成單測
至此,至少從“量”上,Cursor 已經(jīng)能幫助生成比較完整的單測代碼了,并且大部分簡單代碼都能一次性通過。但對于復(fù)雜場景、復(fù)雜組件,生成的代碼通常會(huì)存在一些問題,沒法直接跑通,例如:
圖片
圖片
接下來就需要人工介入修復(fù)這些疑難雜癥了,有一些小技巧:
- 使用 測試框架(vitest/jest 都有提供) 的 [only](https://vitest.dev/api/#describe-only) 接口配合 Filter 能力,只跑存在問題的用例,降低信息噪音;
- 可以使用terminal 右上角的 Add to Composer 按鈕,讓 LLM 繼續(xù)幫你解決問題;
圖片
- 其次,絕大部分問題都出在 Mock 上面,例如下圖所示,錯(cuò)誤看起來是 ESM 與 CommonJS 互相調(diào)用的問題,實(shí)則可以通過 Mock 解決,因此需要有意識 Mock 掉所有下游模塊調(diào)用、環(huán)境變量調(diào)用等(LLM 在這方面做的并不是很好,還是需要比較多人工介入);
圖片
最后
為免遺漏,最后在總結(jié)幾個(gè)必要條件:
- 安裝 Cursor,盡可能充上會(huì)員;
- 開啟 Codebase Indexing;
- 打開 Cursor Agent Yolo 模式,它能自動(dòng)調(diào)用各類 cli 工具,驗(yàn)證生成的代碼是否符合預(yù)期,比如自動(dòng)調(diào)用 lint 檢查代碼風(fēng)格,ts 檢查類型合規(guī),vitest 檢測單測是否通過等;
圖片
- 把上述兩段 Rules 加進(jìn) .cursor/rules 中。
其次,面對復(fù)雜代碼時(shí),LLM 通常無法順利生成對應(yīng)單測,甚至在人類智能介入的情況下,成本依然很高,因此更建議盡可能保持源碼的簡潔性,遵循一些最佳實(shí)踐規(guī)則。