mcpField
A
assertion-quality

assertion-quality

github.com/managedcode测试与质量49637 次安装1 天前更新
代码仓库

关于 assertion-quality

用于分析测试代码断言质量与多样性的技能,可评估任何受支持语言的测试。它通过分类断言并汇总指标,揭示测试是否检查异常、状态变化、结构和负面行为,而不只是单一结果。

主要功能

  1. 01分类测试断言
  2. 02统计断言类别与数量
  3. 03识别无断言和浅层测试
  4. 04检查负面及异常断言
  5. 05分析状态与结构验证

适用场景

  1. 01评估测试是否验证关键行为
  2. 02发现只有浅层断言的测试
  3. 03审查新生成测试的质量
  4. 04汇总测试套件断言指标

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/managedcode/dotnet-skills --skill assertion-quality

安装后在 Claude Code 中输入/assertion-quality确认已加载。

附带文件manifest.json

详细介绍

断言多样性分析

分析任意受支持语言的测试代码,衡量断言的多样性和实际意义。生成指标报告,揭示测试是否验证了正确性的不同方面——不仅检查“输出是否等于 X”,还检查结构、异常、状态转换、副作用和不变量。

语言特定指南:读取调用方提供的或运行时列出的 test-analysis-extensions 目录,然后读取与之匹配的语言文件 (dotnet.md、python.md、typescript.md、go.md 等)。 test-analysis-extensions 仅供参考;不要将其作为技能调用。 如果该扩展包不存在,请使用固定的测试框架和本技能的规则, 报告缺少的参考资料,且不要搜索安装目录。

为什么断言多样性很重要

断言多样性低意味着测试浅显。测试可能通过,但未被断言覆盖的逻辑中仍隐藏着缺陷。常见症状:

问题症状后果
断言过于简单测试只包含 Assert.IsNotNull(result) / assert result is not None / expect(x).toBeDefined()测试通过,却没有验证正确性
只关注单个值总是检查一个字段或返回值未被断言覆盖的逻辑中会漏过缺陷
缺少否定断言从不检查不应发生的情况回归问题可能以假阳性的形式混入
缺少状态检查不验证对象状态变化遗漏副作用或生命周期问题
缺少结构检查只断言顶层值嵌套对象中的缺陷不会被发现
没有断言的测试调用代码却不做验证代码覆盖率会造成误导,带来虚假的安全感

适用场景

  • 用户要求评估断言的质量或深度
  • 用户询问“我的测试是否真的在测试有意义的内容?”
  • 用户想知道测试断言是否过于浅显或简单
  • 用户要求提供断言覆盖率指标或多样性分析
  • 用户怀疑测试虽然通过,却带来了虚假的信心
  • test-engineer Agent(或任何测试生成工作流)在宣布运行完成前,将此技能用作新生成测试的完成前自审步骤

不适用场景

  • 用户想编写新测试(任何语言均使用 code-testing,MSTest 则使用 writing-mstest-tests)
  • 用户想检测断言之外的反模式(使用 test-anti-patterns)
  • 用户想修复或重写断言(直接协助他们)
  • 用户询问代码覆盖率百分比(不在范围内——本技能分析断言质量,而非代码行覆盖率)

输入

输入必需描述
测试代码是一个或多个待分析的测试文件或测试项目目录
生产代码否被测代码,用于评估断言是否覆盖重要行为

工作流

步骤 1:检测语言并加载扩展

识别语言和测试框架。读取所提供的 test-analysis-extensions 目录中 extensions/ 下对应的文件。其他语言的文件名请使用该目录中的信息。必要时仅检查 该已知目录;参考资料不可用会限制证据范围,但不会改变工作区根目录或用户要求的审查内容。

步骤 2:收集测试代码

读取用户提供的所有测试文件。如果用户指定了目录或项目,请使用语言扩展文件中的标记扫描所有测试文件(例如 MSTest 的 [TestMethod]、pytest 的 def test_*、Jest 的 it() / test()、Go 的 func TestXxx)。

第 3 步:对每条断言进行分类

针对每个测试方法,找出所有断言,并将其归入以下与语言无关的类别:

类别验证内容跨语言示例
相等性返回值与预期值匹配Assert.AreEqual(MSTest)、Assert.Equal(xUnit)、assert x == y(pytest)、expect(x).toBe(y)(Jest)、assertEquals(JUnit)、if got != want { t.Error... } / assert.Equal(t, want, got)(Go)、x shouldBe y(Kotest)、Should -Be(Pester)、EXPECT_EQ(GoogleTest)
布尔值条件成立Assert.IsTrue、assert flag(Python)、expect(x).toBeTruthy()(Jest)、assertTrue(JUnit)、assert.True(t, ok)(testify)、x.shouldBeTrue()(Kotest)、Should -BeTrue(Pester)、EXPECT_TRUE
Null / None / Nil值的存在或缺失Assert.IsNull(.NET)、assert x is None(pytest)、expect(x).toBeNull()(Jest)、assertNull(JUnit)、assert.Nil(t, v)(testify)、XCTAssertNil(XCTest)、Should -BeNullOrEmpty(Pester)
异常 / 错误错误处理行为Assert.Throws<T>()、pytest.raises(E)、expect(fn).toThrow(E)、assertThrows<E>、assert.Error(t, err) / assert.ErrorIs、#[should_panic](Rust)、XCTAssertThrowsError、Should -Throw、EXPECT_THROW
类型检查运行时类型正确性Assert.IsInstanceOfType、assert isinstance(x, T)、expect(x).toBeInstanceOf(T)、assertInstanceOf、assert.IsType(t, T{}, v)、assert!(matches!(value, Pattern))(Rust)、Should -BeOfType
字符串文本内容和格式StringAssert.Contains、assert sub in s、expect(s).toMatch(/x/)、assertTrue(s.contains(...))、assert.Contains(t, s, sub)、s shouldContain sub、Should -Match、EXPECT_THAT(s, HasSubstr(...))
集合集合内容和结构CollectionAssert.Contains、assert item in collection、expect(arr).toContain(x)、assertIterableEquals、assert.Contains(t, slice, item)、col shouldContainExactly listOf(...)、Should -Contain、EXPECT_THAT(c, ElementsAre(...))
比较顺序和大小Assert.IsTrue(x > y)、Is.GreaterThan、assert x > y、expect(x).toBeGreaterThan(y)、assertTrue(x > y)、assert.Greater(t, x, y)(testify)
近似值浮点数或基于容差的比较Assert.AreEqual(expected, actual, delta)、pytest.approx(y)、expect(x).toBeCloseTo(y)、assertEquals(x, y, delta)、assert.InDelta(t, x, y, delta)、EXPECT_NEAR、EXPECT_DOUBLE_EQ
否定不应发生的情况Assert.AreNotEqual、assert x != y、expect(x).not.toBe(y)、assertNotEquals、assert.NotEqual(t, x, y)、refute(Minitest / Ruby)、Should -Not -Be
状态 / 副作用状态转换和副作用对象变更后对其属性进行断言;模拟调用验证:mock.Verify(...)(Moq)、mock_method.assert_called_with(...)(Python unittest.mock)、expect(mock).toHaveBeenCalledWith(...)(Jest)、verify(mock).method(...)(Mockito)、Should -Invoke(Pester)、expect { code }.to change(obj, :attr)(RSpec)
结构 / 深度对象深层正确性对支持丰富相等性类型使用 Assert.AreEqual、assertThat(obj).usingRecursiveComparison()(AssertJ)、.toEqual({...})(Jest 深度相等)、cmp.Diff(Go go-cmp)、快照测试(.toMatchSnapshot()、syrupy、SnapshotTesting)、assertThat(col).extracting(...)(AssertJ 链式调用)

一条断言可以归入多个类别(例如,Assert.AreNotEqual 同时属于相等性和否定;expect(mock).toHaveBeenCalledWith(...) 同时属于状态 / 副作用和特定调用断言)。

读取已加载的语言扩展文件,获取确切的框架专用断言 API 列表。

第 4 步:计算指标

计算此测试套件的以下指标:

每项测试的指标
  • 断言数量:每个测试方法中的断言数量
  • 断言类别:每项测试使用的断言类别
整个测试套件的指标
  • 每项测试的平均断言数:断言总数 / 测试方法总数
  • 断言类型分布:整个测试套件中使用的不同断言类别数量(共 12 类)
  • 零断言测试:完全没有断言的测试方法数量及占比
  • 仅含简单断言的测试:所有断言都仅为 null 检查或 Assert.IsTrue(true) 的测试数量及占比——简单断言指不验证有意义的值
  • 含自引用断言的测试:断言将输入与其往返转换或恒等变换后的版本进行比较(例如,Assert.AreEqual(input, Parse(input.ToString()))),或将字段与其自身进行比较(例如,Assert.AreEqual(dto.Name, dto.Name))的测试数量及占比。这类断言是恒真式——验证的是代码连接流程,而非行为。
  • 含否定断言的测试:数量及占比(目标:至少 10% 的测试应验证不应发生的情况)
  • 含异常断言的测试:数量及占比
  • 含状态/副作用断言的测试:数量及占比
  • 含结构/深度断言的测试:数量及占比
  • 单类别测试:仅使用一种断言类别的测试数量及占比
第 5 步:应用校准规则

报告结果前,请校准发现:

  • 在描述匹配器的缺陷前,先评估其判定条件。 对每个较弱的断言,指出一个现实中有缺陷的值或行为,它仍会满足该确切判定条件。对每个被认定为有意义的断言,指出它能约束的行为。如果无法根据测试和现有的生产契约给出这样的反例,就不要臆测断言较弱。
  • 只有真正琐碎的断言才算琐碎。 仅检查 null/None/nil 的断言是琐碎的(Assert.IsNotNull(result)、assert result is not None、expect(x).toBeDefined())。但 null 检查后接有意义的值断言,就不算琐碎——null 检查只是对真正断言的前置保护。只有在测试不包含任何有意义的值断言时,才将其标记为“琐碎”。
  • 遵循 Jest 的确切语义。 toBeDefined() 仅会拒绝 undefined;null 也满足该条件,但只有当 null 是会违反契约的现实结果时才提及。toMatchObject(expected) 会按结构验证预期子集;它既不能证明对象标识相同,也不能证明整个对象完全相等。绝不要声称它能做到这些。
  • 检查有意义条件的布尔断言并非琐碎断言。 Assert.IsTrue(result.IsValid) / assert result.is_valid / expect(result.isValid).toBe(true) 检查的是特定属性——这些都是布尔断言,不是琐碎断言。始终为真的断言(Assert.IsTrue(true)、assert True、expect(true).toBe(true))才是琐碎断言。
  • 精确的构造与映射检查有意义。 测试构造一个对象,并将每个指定属性与独立的预期字面值一一对应,可以捕捉值被交换、遗漏或错误赋值的情况。不要仅仅因为实现是构造函数、record、属性映射或内存存储就降低其评价。
  • 考虑测试的意图。 对无返回值方法的测试,即使只使用一个布尔断言,只要验证了依赖项的状态变化,就是合理的。
  • 异常测试本来就往往只有少量断言。 Assert.ThrowsException<T>(() => ...) / with pytest.raises(E): ... / expect(fn).toThrow(E) / #[should_panic] 可能是唯一的断言——对于专门测试异常的测试而言,这没有问题。不要因断言数量少而扣分。
  • 模拟调用验证和裸断言形式都计入。 将 verify(mock).method(...)(Mockito)、expect(mock).toHaveBeenCalledWith(...)(Jest)、Should -Invoke(Pester)、bare assert(pytest)、if got != want { t.Errorf(...) }(Go)都视为相应类别的真实断言。不要把它们当作缺少框架 API 的问题。
  • 快照断言(.toMatchSnapshot()、syrupy、SnapshotTesting)计为结构/深度断言。若快照已过时或从未更新,应单独指出。
  • 基于属性的测试(Hypothesis 的 @given、proptest!、Kotest 的 forAll)会通过生成的用例隐式产生断言——统计内部断言逻辑,不要统计外围框架结构。
  • 不要混淆多样性与数量。 包含 20 个相等性断言的测试,数量很大但多样性很低。包含一个相等性断言、一个 null 检查和一个异常断言的测试,数量较少但多样性良好。
  • 自我参照的断言不是有意义的相等性检查。 断言输出等于输入的往返结果看起来像真正的相等性断言,但如果被测操作预期是恒等操作,这就是同义反复。应将其与普通相等性断言分开指出。如果测试的目的是验证往返转换(序列化/反序列化、编码/解码),那么该断言有效——但还应使用非平凡输入,验证转换过程。
  • 让建议对应到明确指出的行为。 格式化测试应约束确切的格式化表示;验证测试需要包含被拒绝的输入;往返测试需要使用能检验转义、null/空值处理或其他转换边界的输入。对于每个没有断言的创建/更新/删除操作,应分别建议验证其特定返回值或可观察到的后置条件,而不要一概建议“检查状态”。
  • 如果断言的多样性良好,就明确说明。 报告得出测试套件多样性良好的结论,完全合理。
步骤 6:报告发现

根据测试套件的规模和复杂程度调整报告深度。 下方结构是大型测试套件(约 15 个以上测试或多文件项目)的完整模板。对于较小或较简单的输入(单个文件且只有少量测试),不要输出所有章节——在简单输入上铺开多章节仪表盘只会徒增噪声、掩盖答案。应直接、简洁地回答用户的问题:哪些测试没有断言或只有简单断言、整体断言质量如何,以及具体的改进建议(仍需区分有意编写的冒烟测试与伪装成真实验证的测试)。只使用对当前输入有实际价值的章节;简短的指标摘要加上无断言测试列表和建议,往往就足够了。切勿遗漏与评估标准相关的实质内容(无断言/简单断言测试的识别、质量结论和具体建议)——只精简不提供信息的结构性内容。

对于包含五到八个测试的文件,默认只提供一个结论和一张简洁的逐测试表格。 除非调用方要求提供指标,否则省略类别分布仪表盘和假设的失败模式。只陈述由断言谓词和现有生产代码行为支持的反例。

按以下结构呈现分析:

  1. 摘要仪表盘——关键指标速览表:

    | Metric                        | Value  | Assessment |
    |-------------------------------|--------|------------|
    | Total tests                   | 25     | —          |
    | Average assertions per test   | 2.4    | Moderate   |
    | Assertion type spread         | 5/12   | Low        |
    | Tests with zero assertions    | 3 (12%)| Concerning |
    | Tests with only trivial asserts | 4 (16%)| Acceptable |
    | Tests with negative assertions | 2 (8%) | Below target |
    | Single-category tests         | 15 (60%)| High       |
    
  2. 类别细分——针对每种断言类别,展示:

    • 使用该类别的测试数量
    • 代码中的代表性示例
    • 相对于被测代码,使用是否过多或过少
  3. 差距分析——根据生产代码(如果可用),指出:

    • 已测试但仅使用相等性检查的行为
    • 没有异常断言的错误路径
    • 没有状态验证的状态变更方法
    • 已返回但从未检查内容的集合
  4. 建议——按优先级排列的改进建议:

    • 哪些测试最值得增加其他类型的断言
    • 缺少哪些断言类别,以及为何重要
    • 可添加的断言具体示例
  5. 无断言测试——如果存在,逐一列出每个测试的方法名及其似乎在测试的内容,以便用户决定添加断言或将其标记为有意编写的冒烟测试。

验证

  • 测试套件中的每个断言都至少归入一个类别
  • 指标计算正确(数量总和一致)
  • 正确识别只有简单断言的测试(不过度标记)
  • 不因断言数量较少而惩罚异常测试
  • 不将针对有意义属性的布尔断言归类为简单断言
  • 每项关于断言较弱的判断都包含一个现实的反例,且该反例会被 精确的匹配器接受
  • Jest 匹配器语义准确(toBeDefined 与 undefined; toMatchObject 的子集匹配与身份/完全相等)
  • 建议具体明确(点名特定测试方法,并建议具体的断言类型)
  • 如果测试套件涵盖的断言类型丰富,报告应予以肯定

常见陷阱

陷阱解决方案
因断言数量较少而惩罚异常测试异常断言本身就是完整的——对此类测试跳过数量警告
将先于值检查的 null/None/nil 检查判定为无意义仅当 null/None/nil 检查是唯一断言时,才标记此类测试
将任何布尔断言都视为无意义只有恒为真的断言(Assert.IsTrue(true)、assert True、expect(true).toBe(true))才是无意义的
忽略框架差异每种框架都有不同的断言 API——务必先阅读对应的语言扩展。MSTest 的 Assert.AreEqual、xUnit 的 Assert.Equal、NUnit 的 Is.EqualTo、pytest 中裸写的 assert ==、Jest 的 expect().toBe()、Go 的 if … { t.Error… } 都属于 Equality 类别
将裸断言形式视为缺少框架支持裸写的 assert(pytest)、if got != want { t.Error... }(Go)和 assert!()(Rust)都是标准形式——应将它们计入正确的类别
将 Mock 调用验证视为没有断言verify(mock).method(...)、expect(mock).toHaveBeenCalledWith(...)、Should -Invoke 都属于状态/副作用断言
为了多样性而建议增加多样性仅建议添加能够发现被测代码实际错误的断言类型
遗漏隐式断言异常断言同时属于 Exception 和 Negative;快照测试/基于属性的测试都是真实断言,只是结构隐式
包含未等待断言的异步测试TUnit、Jest 的 .resolves/.rejects、pytest-asyncio、Swift Testing 和 Kotest 都会静默通过未使用 await 等待断言的测试——即使存在断言调用,也应将其视为没有断言

评论

支持 Markdown