soarli

羊毛薅到极致:用即将过期的 DeepSeek API 为 Node.js 项目批量生成单元测试
🎯 背景与动机当手里的大模型 API 额度(如 DeepSeek V3.2)马上过期,如何快速、高价值地“压榨”剩...
扫描右侧二维码阅读全文
27
2026/03

羊毛薅到极致:用即将过期的 DeepSeek API 为 Node.js 项目批量生成单元测试

🎯 背景与动机

当手里的大模型 API 额度(如 DeepSeek V3.2)马上过期,如何快速、高价值地“压榨”剩余 Token? 手动对话太慢,直接让 AI 重构核心代码又存在极高的线上风险。经过评估,全项目批量生成单元测试是性价比最高、最安全的“耗 Token 巨兽”方案:

  1. 绝对安全(非侵入性):生成的代码放在独立文件夹,不会改动任何原有业务逻辑。
  2. 极高吞吐量:单测代码(包含 Mock 和边界断言)通常比业务代码更长,能快速消耗额度。
  3. 偿还技术债:借此机会将项目的测试覆盖率从 0% 强行拉升,为后续重构打下坚实基础。

🛠️ 方案架构

我们的目标项目是一个标准的 Node.js(Express/Koa)后端服务,包含 controllersservicesmodelsmiddlewaresutils 等目录。

整体工作流:

  1. 编写一个 Python 异步高并发脚本,读取指定业务目录下的 .js/.ts 文件。
  2. 构造严密的 System Prompt,调用硅基流动(SiliconFlow)的 DeepSeek API。
  3. 将生成的测试文件(基于 Jest 框架)安全地输出到全新的 ai_generated_tests 隔离目录中。
  4. 在 Node.js 项目中配置 Jest,运行并验收这些 AI 生成的测试用例。

💻 第一步:核心驱动器(Python 并发脚本)

在 Node.js 项目的根目录下,新建一个 burn_api.py 文件。这个脚本是消耗 API 的核心引擎。

环境准备

确保电脑已安装 Python 3.8+,并在终端安装 OpenAI 官方 SDK(DeepSeek 完全兼容):

pip install openai

核心代码 (burn_api.py)

import os
import asyncio
from openai import AsyncOpenAI

# ================= 配置区 =================
# ⚠️ 警告:切勿将包含真实 Key 的代码提交到 Git 公开仓库!跑完后建议去后台作废!
API_KEY = "你的_真实_API_KEY"
BASE_URL = "[https://api.siliconflow.cn/v1](https://api.siliconflow.cn/v1)" 
MODEL = "deepseek-ai/DeepSeek-V3" 

# 核心业务逻辑文件夹(精准狙击,跳过 views/docs 等无关目录)
TARGET_DIRS = ["controllers", "services", "models", "middlewares", "utils"]
# 隔离生成的测试代码,防止覆盖原有代码
TEST_DIR = "./ai_generated_tests"      
FILE_EXTENSIONS = ('.js', '.ts') 
TEST_SUFFIX = ".test"     

# 并发数控制(若遇 429 Too Many Requests 报错,请调低至 2 或 3)
CONCURRENCY_LIMIT = 5     
# ==========================================

client = AsyncOpenAI(api_key=API_KEY, base_url=BASE_URL)

SYSTEM_PROMPT = """你是一个资深的 Node.js 测试开发工程师。请为以下代码编写严密的单元测试。
要求:
1. 使用 Jest 测试框架。
2. 覆盖正常逻辑(Happy Path)与异常边界条件(Edge Cases)。
3. 如果代码中涉及数据库(Mongoose/Sequelize等)、外部网络请求或文件系统,必须使用 jest.mock() 进行深度 Mock,绝不能发起真实请求。
4. 使用 describe 块组织测试套件,用 it() 提供清晰的中文测试用例说明。
严格限制:你必须且只能输出纯测试代码本身!不要包含任何解释、问候语或 Markdown 代码块标记(如 ```javascript)。"""

async def generate_tests(file_path, content, semaphore):
    async with semaphore:
        print(f"🧪 正在请求 API 生成单测: {file_path}...")
        try:
            response = await client.chat.completions.create(
                model=MODEL,
                messages=[
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": content}
                ],
                temperature=0.1, # 保持极低温度,确保测试代码逻辑严谨
                max_tokens=4000
            )
            return response.choices[0].message.content
        except Exception as e:
            print(f"❌ 处理 {file_path} 失败: {e}")
            return None

async def process_file(base_dir, file_name, semaphore):
    file_path = os.path.join(base_dir, file_name)
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    if not content.strip(): return

    test_content = await generate_tests(file_path, content, semaphore)

    if test_content:
        # 清理可能残留的 Markdown 标记
        if test_content.startswith("```"):
            test_content = "\n".join(test_content.split("\n")[1:])
        if test_content.endswith("```"):
            test_content = "\n".join(test_content.split("\n")[:-1])

        # 保持原有目录结构映射(例:controllers/user.js -> ai_generated_tests/controllers/user.test.js)
        name, ext = os.path.splitext(file_name)
        test_file_name = f"{name}{TEST_SUFFIX}{ext}"
        
        out_dir = os.path.join(TEST_DIR, base_dir)
        os.makedirs(out_dir, exist_ok=True)
        out_file_path = os.path.join(out_dir, test_file_name)
        
        with open(out_file_path, 'w', encoding='utf-8') as f:
            f.write(test_content)
        print(f"✅ 成功保存: {out_file_path}")

async def main():
    if not os.path.exists(TEST_DIR): os.makedirs(TEST_DIR)
    semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT)
    tasks = []

    for target in TARGET_DIRS:
        if not os.path.exists(target): continue
        for root, _, files in os.walk(target):
            for file in files:
                if file.endswith(FILE_EXTENSIONS) and TEST_SUFFIX not in file:
                    tasks.append(process_file(root, file, semaphore))

    if not tasks:
        print("没有找到需要处理的代码文件,请检查目录是否正确。")
        return

    print(f"🚀 共找到 {len(tasks)} 个文件,开始高并发生成...")
    await asyncio.gather(*tasks)
    print("\n🎉 全部单元测试生成完毕!API 额度燃烧成功!")

if __name__ == "__main__":
    asyncio.run(main())

运行脚本

在终端执行:

python burn_api.py

坐和放宽,看着终端飞速滚动的绿字,享受薅羊毛的快感。

⚙️ 第二步:Node.js 侧 Jest 配置与运行

等 Python 脚本跑完后,你的项目根目录下会多出一个 ai_generated_tests 文件夹。接下来要把它们跑起来。

1. 安装测试依赖

在项目根目录终端运行:

npm install --save-dev jest supertest

(注:supertest 常用于辅助测试 Express/Koa 的 HTTP 接口)

2. 配置 package.json

打开 package.json,在 scripts 节点下新增一条专属的 AI 测试命令,并开启覆盖率报告:

"scripts": {
  "start": "node app.js",
  "test:ai": "jest ./ai_generated_tests --coverage"
}

3. 一键验收

运行测试命令:

npm run test:ai

🚨 第三步:常见报错与人工微调指南(填坑必看)

AI 生成的代码不可能 100% 完美运行,第一次运行 npm run test:ai 时满屏红字报错是绝对正常的。这是因为 AI 缺乏全局上下文,主要会犯以下几种错误,需要人工顺手修复:

坑位 1:Require/Import 相对路径错误(占 80%)

大模型在生成 ai_generated_tests/controllers/user.test.js 时,不知道真正的 user.js 在几层目录之外。

  • 报错现象Cannot find module '../../src/controllers/user'
  • 修复方案:打开报错的测试文件,将顶部的引入路径修改为正确的真实相对路径。例如改为 ../../controllers/user.js

坑位 2:缺乏真实的 App 实例

测试 Controller 时,AI 经常会自作主张写 const app = require('../../app')。如果你的 app.js 没有导出实例,就会报错。

  • 报错现象app.get is not a function 或找不到路由。
  • 修复方案:确保你的主入口文件(如 app.js)底部有 module.exports = app;,并在测试文件中正确引入它。

坑位 3:Mock 不彻底导致连真库超时

虽然 Prompt 里强调了必须 Mock 数据库,但偶尔它会漏掉某些深层依赖,导致测试一直挂起(Pending)或连接被拒绝。

  • 报错现象connect ECONNREFUSEDTimeout - Async callback was not invoked
  • 修复方案:在测试文件顶部手动强制 Mock 数据模型,例如 jest.mock('../../models/user');

💡 验收策略建议

不要试图一次性解决几百个文件的报错。建议的流程是:

  1. 先挑软柿子捏:先单独跑 Utils 或纯函数的测试(npx jest ./ai_generated_tests/utils/xxx.test.js),这些通常改个路径就能 100% Pass。
  2. 逐个攻破业务层:再慢慢排查 Services,最后处理逻辑最复杂的 Controllers。

🔒 终极安全警告

跑完这套流程后,请务必、立刻、马上登录硅基流动(SiliconFlow)控制台,将本次使用的 API Key 删除或重置! 由于脚本曾配置过明文 Key,为了防止任何意外泄露导致额度被恶意盗刷,销毁旧 Key 是程序员的基本安全素养。

最后修改:2026 年 03 月 27 日 02 : 44 AM

发表评论