vinicq/falsegreen-js

GitHub: vinicq/falsegreen-js

一款基于 AST 静态扫描的 JavaScript/TypeScript 测试质量工具,用于检测表面上通过但实际未验证任何内容的「假绿」测试。

Stars: 1 | Forks: 0

# falsegreen-js 查找产生误报(即没有任何保护作用的通过测试,以及在断言错误内容时依然通过的测试)的 JavaScript/TypeScript 单元测试。采用确定性的 AST 扫描,不执行代码。它是 [`falsegreen`](https://github.com/vinicq/falsegreen)(Python 扫描器)的兄弟项目;具有相同的契约,但使用 JS/TS 规则集。 支持 `.js`、`.jsx`、`.ts`、`.tsx`、`.mjs`、`.cjs`、`.mts`、`.cts`。 ## 为什么 一个测试可能会显示通过却没有任何保护作用:比如空函数体、永远不会执行到的断言、`expect(x).toBe(x)`、没有匹配器的 `expect(value)`、一个会让测试套件的其余部分静默失效的聚焦测试 `it.only`,或者一个从未被 await 的 `findByText`。AI 生成的测试会大量产生这些问题。该工具会在这些机械模式进入代码审查之前,标记出解析器能够证明的问题。 ## 安装 ``` npm install -D falsegreen-js ``` ## 用法 ``` npx falsegreen-js # scan cwd npx falsegreen-js src test # scan paths npx falsegreen-js --staged # only test files staged in git (pre-commit) npx falsegreen-js --json # machine-readable output npx falsegreen-js --output report.json # write to a file npx falsegreen-js --output .falsegreen/ # write report. into a directory npx falsegreen-js --disable C7,JS3 ``` 每个发现的问题都会报告其测试金字塔层级(单元 / 集成 / e2e,从文件的 import 中读取)以及一行修复提示,并且摘要会按层级细分发现的问题并列出最常见的修复方法。`--output` 接收一个文件或目录:对于没有扩展名或带有斜杠的路径(例如 `.falsegreen/`),会根据所选格式生成 `report.` 文件。报告属于运行产物;请将输出目录添加到 gitignore 中。 退出代码:`0` 表示干净,`10` 表示仅有低置信度问题,`20` 表示存在高置信度问题。将其接入 CI 或 pre-commit 钩子中,让退出代码 `20` 阻止提交。 内联屏蔽单个发现的问题: ``` expect(user.id).toBe(user.id); // falsegreen: ignore[C7] expect(x); // falsegreen: ignore ``` ## Runner 覆盖范围 与 Runner 无关。断言和测试词汇涵盖了 Jest、Vitest、Mocha + Chai、Jasmine、AVA、`node:test`、tap、Cypress、Playwright、Testing Library(带有 `jest-dom` / `jasmine-dom` 匹配器和 `user-event` 的 `@testing-library/*`),以及 Vue Test Utils(`mount`/`wrapper.find`/`flushPromises`/`nextTick`)。 `expect().matcher()`、chai 的 `expect().to`、`assert`、`x.should` 和 AVA 的 `t.is` 都算作真正的断言,因此 Mocha 或 AVA 测试不会被误认为是根本没有检查任何内容的测试。 注意:组件文件(`.vue`、`.svelte`、`.astro`、`.marko`)和模板文件(`.html`)不是测试文件。针对这些框架的测试是使用上述八种扩展名的 `.spec`/`.test` 文件编写的,这正是扫描器所读取的内容。 ## 测试层级(金字塔) falsegreen-js 会扫描金字塔每个层级的测试。发现过程是与层级无关的——它会读取任何测试文件——但少数代码会结合层级进行解读,因此在一个层级上是有效模式的代码,在另一个层级上就不会被标记。 - **单元测试:** 一个带有被替身边界的函数或组件。验证依据是 `expect`。 - **集成测试(API 和数据库):** 通过 supertest / chai-http(如 `request(app).get("/").expect(200)`,会被识别为断言)或 `fetch` 进行的 API 测试,以及通过 Prisma / TypeORM / Knex 针对真实数据存储进行的数据库测试。这些测试故意跨越 I/O 边界,因此响应或数据行本身就是该层级的验证。 - **E2E:** Cypress (`.cy.*`) 和 Playwright (`.e2e.*`)。`cy.get().should(...)` 和 `expect(page).toHaveURL(...)` 是验证依据;在这里,一个可见的元素是真正的检查,而不是弱检查。 在一个声称是单元测试的测试中出现真实的 API 或数据库调用,本身就是一种代码异味(mystery guest,环境耦合),而不属于该测试的层级问题。C23 会标记硬编码的文件路径或 URL 形式。 ## 问题代码目录 与 `falsegreen`(Python 版)共享的代码会保持相同的 ID,因此跨语言的研究结果可以对应一致。`JS*` 代码是特定于该生态系统的。 | 代码 | 置信度 | 标记内容 | |---|---|---| | C2 | 高 | 完全没有检查的测试(空函数体) | | C2b | 低 | 测试调用了代码但没有断言任何内容 | | C5 | 高 | 永远为真的检查(`expect(true).toBe(true)`,`assert(1)`) | | C6 | 低 | 弱检查 —— 仅验证返回了内容(`toBeTruthy`/`toBeDefined`,`length > 0`) | | C7 | 高 | 将事物与其自身进行比较(`expect(x).toBe(x)`) | | C20 | 高 | `return`/`throw` 后死代码中的断言 —— 永远不会执行 | | C23 | 低 | 读取字面量路径的真实文件,或硬编码的 URL(mystery guest) | | C8 | 低 | 对浮点数进行严格相等比较(应使用 `toBeCloseTo`) | | C9 | 低 | `toThrow()` 没有错误类型或消息 —— 接受任何错误 | | C16 | 低 | 结果依赖于 `Date.now`、`Math.random` 或固定的计时器 | | C18 | 低 | 将 `String(x)` / `JSON.stringify(x)` / `` `${x}` `` 与字面量进行比较(检查的是格式,而不是值) | | C21 | 低 | 每一个断言都是有条件的 —— 没有任何断言是无条件执行的 | | C37 | 低 | `it.each`/`test.each` 中的重复用例 —— 相同的场景运行两次 | | CC | 低 | 被注释掉的断言 | | JS1 | 高 | 聚焦测试(`it.only` / `fit`)静默跳过了测试套件的其余部分 | | JS2 | 高 | `expect(x)` 没有匹配器 —— 断言永远不会执行 | | JS3 | 低 | 快照是唯一的断言 | | JS4 | 低 | 被跳过的测试(`it.skip` / `xit` / `it.todo`)永远不会执行 | | JS5 | 低 | 未被 await 的异步查询/事件(`findBy*` / `waitFor` / `user-event`) | | JS6 | 高 | 空的 `describe`/`suite` —— 测试套件显示通过但没有运行任何内容 | | JS7 | 低 | 非 awaited 的 `setTimeout`/`then` 回调中的断言 —— 可能在测试结束后才运行 | | JS8 | 低 | 模拟被测试的单元(直接断言导入模块的 `jest.mock`/`vi.mock`) | | JS9 | 高 | 死分支中的断言(`if(false)` / `if(true){}else`) —— 永远不会执行 | | JS11 | 低 | `try/catch` 吞没了断言 —— 失败的 `expect` 被捕获,测试保持通过 | | JS13 | 低 | 查询(`getBy*`/`queryBy*`)作为松散语句 —— 其结果从未被断言 | | JS15 | 低 | 不恰当的断言 —— 包装在布尔值中的比较(`expect(a===b).toBe(true)`),盲目的失败消息 | | JS17 | 低 | 被注释掉的测试块(`// it(...)` / `// test(...)`) —— 已禁用,不再运行 | | JS18 | 低 | 测试使用 `done` 回调而不是 async/await —— 时机不对的 `done` 会提前通过 | | JS21 | 高 | 引用了匹配器但从未调用(没有 `()` 的 `expect(x).toBe`) —— 断言永远不会执行 | | JS22 | 高 | 空的 `it.each`/`test.each` 表 —— 生成了零个用例,永远不会执行 | 每个代码都带有一个判断标签(J1-J6),与 [falsegreen-skill](https://github.com/vinicq/falsegreen-skill) 语义框架共享。 ### 可选开启:可维护性分组(默认关闭) 这些**不是**误报(假绿)—— 测试仍然具有保护作用 —— 因此它们默认是关闭的。使用 `--diagnostics` 启用它们,或者通过配置 `severity` 为每个代码单独启用。它们是测试代码健康的“加分项”,映射了 falsegreen 的诊断/耦合分组。 | 代码 | 分组 | 标记内容 | |---|---|---| | D1 | 诊断 | 断言轮盘 —— 一个测试中有大量断言 | | D3 | 诊断 | 重复断言 —— 重复了相同的断言 | | D4 | 诊断 | 没有带标题用例的 `it.each`/`test.each`(仅使用索引) | | D6 | 诊断 | 测试主体中的 `console.*` | | D7 | 诊断 | 匿名测试 —— 空的或缺失描述 | | D8 | 诊断 | 魔术数字 —— 作为期望值的纯数字字面量 | | M2 | 耦合 | 测试主体超过了代码行数阈值 | ``` npx falsegreen-js --diagnostics # include D*/M* as warnings ``` ### 故意未实现 目录中的某些代码经过审查后被故意排除: - **JS19**(在对象/数组字面量上使用 `toBe`):`expect(x).toBe({...})` 是按引用比较的,所以总是会失败。那是一个明显的失败(红)测试,与误报(假绿)正好相反,不在本工具范围内。 - **JS20**(在没有使用 `resolves`/`rejects` 的情况下比较 Promise):判断一个值是否为 Promise 需要解析器所不具备的类型信息,因此误报率会太高。 - **JS12**(其 `expect` 从未返回的 floating promise):已被 JS7 涵盖。 - **JS16**(没有 `expect.assertions(n)` 的 `async` 测试):缺少守卫本身并不是代码异味;标记它会在大多数异步测试中触发。 - **JS10**(测试主体中的任何条件判断):由 `eslint-plugin-jest`(`no-conditional-in-test`)处理;JS9 和 C21 已经涵盖了产生误报的部分。 ### 从 falsegreen 继承的内容,以及未继承的内容 已移植(相同概念):C2, C2b, C5, C7, C8, C16, CC。 仅限 Python,不适用于 JS/TS:pytest 收集规则(C4 系列),`pytest.raises` 广度(C9/C19/C27/C28),fixture 和 `os.environ`/全局状态代码(C23/C24/C29),sklearn/torch/tensorflow 指标和种子代码(C33,部分 C16),xfail(C25),以及 xunit/`self.assert*` 代码。这些在 JS 中没有对应项,或者需要不同的信号。 仅限 JS/TS(此处为新增):上文的 JS1-JS5。`describe.only`/skip、快照、无匹配器以及未被 await 的模式是 JS 测试 Runner 和 Testing Library 所特有的。 ## 配置 可选。支持 `falsegreen.json`、`.falsegreenrc.json`,或 `package.json` 中的 `"falsegreen"` 键: ``` { "disable": ["C8"], "exclude": ["**/legacy/**"], "severity": { "JS3": "off", "C16": "high" } } ``` 优先级:CLI `--disable` > 配置 `disable`/`severity` > 目录默认值。 ## 范围与诚实 这是一个静态扫描器。它只处理结构能够证明的内容。它不确定两件事:期望值是否与预期行为相矛盾,以及测试是否重新实现了生产逻辑。这些属于语义层面的内容,应交给 `falsegreen-skill` LLM 处理。宁可精确不要高召回:宁愿选择一种会漏掉某些用例的温和启发式方法,也不选择会标记正确代码的方法。 ## 参考文献 该目录建立在测试异味文献的基础之上。直接影响包括:为整个问题家族命名的 rotten-green-test 研究(Delplanque 等人,ICSE 2019),开创性的测试异味重构目录(van Deursen 等人,XP 2001),JS/TS 实证研究(Jorge,UFCG 2023;Silva,PUC Minas 2022 —— 聚焦测试和快照代码的学术先例;Oliveira 等人,SBES 2024/2025),以及检测工具基线(tsDetect,Peruma 等人,2020)。完整列表以及代码到来源的映射详见 [CREDITS.md](CREDITS.md)。 ## 状态 `0.1.0`,早期版本。规则集是确定性的核心;完整的 JS/TS 异味目录作为研究在私有审计中心进行追踪。欢迎提交 Issue 和 PR。 ## 许可证 MIT,Vinicius Queiroz。 新的贡献者会自动添加;该表格还根据 [all-contrib](https://allcontributors.org) 规范认可非代码工作(文档、创意、基础设施、测试、研究)。
标签:MITM代理, TypeScript, 单元测试, 安全插件, 暗色界面, 自动化攻击, 错误基检测, 静态代码分析