写作与公文
在修复或现代化现有 MSTest 测试时…
当需要修复、重写、更新或解释现有 MSTest 测试及 MSTest 配置时,依据项目实际安装的 MSTest 版本给出可编译的现代化改法。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在修复或现代化现有 MSTest 测试时通用
解决思路
- 先检查测试项目、packages.config 和程序集引用 HintPath,确定确切的 MSTest 版本与项目系统:MSTest.Sdk 从项目 SDK 声明或 global.json 的 msbuild-sdks 解析版本;MSTest 元包解析包版本;MSTest.TestFramework + MSTest.TestAdapter 检查版本;经典非 SDK XML 或 packages.config 项目保留原项目系统并把每个新测试文件加入 <Compile Include>。
- 检查代表性测试中的自定义基类夹具、辅助库、mock 语法、命名、setup 和数据构建器;现有约定与已安装版本优先于示例,除非用户明确要求迁移,否则不要升级 MSTest、Moq、NBuilder 或项目格式。
- 把 API 可用性当作硬性门槛:Assert.ThrowsExactly*、统一 Assert.Contains/HasCount/IsEmpty/IsNotEmpty 需 3.8+;Assert.IsGreaterThan/IsLessThan/IsInRange/StartsWith/EndsWith/MatchesRegex 需 3.10+;泛型 Assert.IsInstanceOfType<T>(value, out var typed) 仅 3.4-3.11;ValueTuple DynamicData 需 3.7+;构造函数注入 TestContext 需 3.6+;[Retry]、[OSCondition] 需 3.8+;[CICondition] 需 3.10+;不满足最低版本时改用兼容回退(Assert.ThrowsException*、CollectionAssert、StringAssert、Assert.IsTrue 加清晰消息、IEnumerable<object[]>、实例 TestContext 属性等)。
- 仅对全新项目推荐 MSTest.Sdk 或 MSTest 元包:MSTest.Sdk 写法为 <Project Sdk="MSTest.Sdk"> 并在 global.json 的 msbuild-sdks 中统一写版本;元包写法为 <PackageReference Include="MSTest" Version="3.8.2" />。
- 按约定编写测试类:用 sealed 密封测试类,类上加 [TestClass]、方法上加 [TestMethod],遵循 Arrange-Act-Assert(AAA)模式,命名用 MethodName_Scenario_ExpectedBehavior,测试项目命名用 [ProjectName].Tests;仅在与会话既有基类和生命周期不冲突时应用。
- 选用已安装版本支持的最具体断言:相等用 Assert.AreEqual(expected, actual),引用同一性用 Assert.AreSame,空值用 Assert.IsNull/IsNotNull,集合空/非空用 Assert.IsEmpty/IsNotEmpty(3.8+)或 CollectionAssert/计数断言,集合数量用 Assert.HasCount(N, collection)(3.8+),包含用 Assert.Contains/DoesNotContain(3.8+)或 CollectionAssert,类型用 Assert.IsInstanceOfType<T>,异常用 Assert.ThrowsExactly<T>(3.8+)或 Assert.ThrowsException<T>(更早版本)。
- 在 MSTest 3.8+ 上,两者都存在时优先用 Assert 类方法而非 StringAssert 或 CollectionAssert;更早版本保留兼容的专用类。
- 当请求涉及多个独立集合属性时,即使某个断言隐含了另一个,也要保留每个语义检查:例如需要区分空/非空时保留 IsNotEmpty,再用 HasCount 和 ContainsSingle 分别保证基数。
- 异常测试把抛异常操作放进 lambda,区分 ThrowsExactly<T>(精确类型)与 Throws<T>(类型或派生类型),当 ParamName 等属性属于行为一部分时捕获返回的异常;异步用 await Assert.ThrowsExactlyAsync<T>(...)。
- 把泛型 Assert.IsTrue 替换为专用断言以获得更好失败消息:list.Count > 0 用 IsNotEmpty,list.Count == 0 用 IsEmpty,list.Count() == 3 用 HasCount(3, list),x != null 用 IsNotNull,x == null 用 IsNull,同一实例用 AreSame,!list.Contains(item) 用 DoesNotContain,list.Single(predicate) + IsNotNull 用 ContainsSingle,list.Contains(item) 用 Contains。
- 字符串断言在 3.10+ 用 Assert.Contains/StartsWith/EndsWith/MatchesRegex,更早版本用 StringAssert.Contains/StartsWith/EndsWith/Matches。
- 类型断言按已安装次版本选择形式:3.0-3.3 用 Assert.IsInstanceOfType(result, typeof(MyHandler)) 后安全强转;3.4-3.11 用 Assert.IsInstanceOfType<MyHandler>(result, out var typed);4.x 用 var typed = Assert.IsInstanceOfType<MyHandler>(result)。
- 比较断言保持条件语义,期望边界在前、观测值在后:score > 0 写成 Assert.IsGreaterThan(0, score),score < 100 写成 Assert.IsLessThan(100, score),score >= 60 && score <= 90 写成 Assert.IsInRange(60, 90, score);不要为模仿源码从左到右的顺序而反转参数。
- 数据驱动测试:内联值用 [DataRow];复杂数据在 3.7+ 优先用 ValueTuple 返回类型(如 IEnumerable<(decimal price, int percent, decimal expected)>)而非 IEnumerable<object[]>,更早版本保留 IEnumerable<object[]>;需要自定义显示名或元数据时在 3.8+ 用 TestDataRow<T>。
- 生命周期:现有套件支持时优先构造函数初始化,保留共享的 FixtureBase<TSut> 或既有 [TestInitialize] 生命周期,不要顺带重写夹具架构;[TestInitialize] 仅用于异步初始化,同步部分放构造函数;[TestCleanup] 用于即使失败也必须运行的清理;仅在 3.6+ 通过构造函数注入 TestContext,否则用实例属性。
- 执行顺序:AssemblyInitialize(每程序集一次)→ ClassInitialize(每类一次)→ 每个测试(属性注入时:构造函数 → 设置 TestContext 属性 → TestInitialize;构造函数注入时:构造函数接收 TestContext → TestInitialize)→ 测试方法 → TestCleanup → DisposeAsync → Dispose(每测试)→ ClassCleanup(每类一次)→ AssemblyCleanup(每程序集一次)。
- 取消与超时:3.11+ 用 TestContext.CancellationToken 配合 [Timeout(milliseconds, CooperativeCancellation = true)];3.6.4-3.10 用 TestContext.CancellationTokenSource.Token 配合协作式取消;普通 [Timeout] 不能保证框架令牌会停止进行中的工作;更早版本在取消本身是被测行为时使用测试自有的 CancellationTokenSource。
- 重试仅用于真正不稳定的外部依赖(网络、文件系统),不要用来掩盖竞态或共享状态问题;对外部服务使用有界次数加非零延迟/退避,例如 [Retry(3, MillisecondsDelayBetweenRetries = 1_000, BackoffType = DelayBackoffType.Exponential)]。
- 条件执行:OSCondition 需 3.8+,CICondition 需 3.10+;用 [OSCondition(OperatingSystems.Windows)]、[CICondition(ConditionMode.Exclude)] 等特性替代测试体内的环境分支,但不要替代被测操作;修正代码时保留真实的注册表/GPU/服务操作和具体资源清理,不要返回空方法或仅注释占位,清理状态要安全初始化并对称释放(setup 可能失败时加 null 保护)。
- 并行化:程序集级用 [assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)],需要退出的类加 [DoNotParallelize]。
- 修复 MSTESTxxxx 分析器诊断:按常见诊断表应用惯用且版本兼容的改法并重新构建确认诊断消失,不要抑制规则;例如 MSTEST0006 用 Assert.Throws/ThrowsExactly 替换 [ExpectedException],MSTEST0017 把 expected 放前 actual 放后,MSTEST0023 用 Assert.IsFalse,MSTEST0025 用 Assert.Fail("reason"),MSTEST0032 删除或修正断言,MSTEST0037 改用具体断言,MSTEST0038 值类型改用 Assert.AreEqual,MSTEST0039 在 3.8+ 改用 Assert.Throws/ThrowsExactly(含 Async 变体),MSTEST0044 在支持数据行的版本上把 [DataTestMethod] 换成 [TestMethod],MSTEST0046 在 3.10+ 用等价 Assert 方法,MSTEST0052 去掉显式 DynamicDataSourceType,MSTEST0042/MSTEST0060 删除重复特性,MSTEST0024 把静态 TestContext 改为实例成员,MSTEST0045/0049/0054 把 TestContext.CancellationToken 流入被等待调用,MSTEST0036 重命名或用 override 替代 new,MSTEST0061 改用 [OSCondition(...)],MSTEST0002/0003/0005/0007–0014 按规则修正签名(如改为 public、修正返回类型和参数、按需加 static);表未列出的规则查阅 https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/overview。
- 用 MSTestAnalysisMode MSBuild 属性(3.8+)全局控制规则集:None | Default | Recommended | All,多数项目应采用 Recommended;少数完全可选规则(如 MSTEST0015、MSTEST0019–0022)需要时通过 .editorconfig 按项目启用;优先修复底层代码而非抑制诊断,仅在记录理由后抑制。
- 用户要求仓库编辑且未禁止执行时,编辑后运行最窄范围的 dotnet test 命令验证;进程成功但没有发现测试数量不算验证,必须确认目标测试用例被发现并通过;若编译暴露直接耦合的源问题(如缺少命名空间导入),只做最小修复并重跑,不要升级包或扩大现代化范围;报告实际测试数量和所做修复,绝不把未运行或无输出的测试说成通过。
- 最终交付时把每项请求的现代化映射到确切修正后的构造,并引用通过的测试命令;当 expected/actual 顺序、精确类型检查、数据发现或类形状是明确要求时,不要用笼统的“已现代化”总结代替。
- 特定 API 或模式问题(断言、数据驱动、生命周期)直接跳到相关工作流步骤,不要走完整流程;从零生成新测试交给 code-testing-agent,本技能仅作 MSTest API/版本支持;审查并修复现有测试时只修存在的问题,不添加无关改进;断言转换先展示修正后的调用,再用一句话说明语义原因,对 Assert.AreEqual 先写 expected 后写 actual 并解释这保留了 Expected/Actual 失败标签;提供的代码请求返回完整有代表性的方法体而非仅注释占位。
- 对每个允许只读文件检查的仓库范围任务,先检查仓库根目录的 .agents/skill-overlays/dotnet-test/writing-mstest-tests.md;若存在,先读一次并应用其仓库特定的命名、布局、框架与策略绑定;要求其 frontmatter 声明 core: dotnet-test/writing-mstest-tests、binding-revision: "1"、mode: extend,任一缺失或不符则报告不匹配、忽略 overlay 并继续使用本技能的通用指导;用户明确指令和已验证的项目约束优先于 overlay,overlay 优先于本技能的通用默认值和示例;文件存在但不可读或与仓库冲突时报告问题、忽略 overlay 并继续;不存在则正常继续;仅当任务与仓库无关或用户明确禁止所有文件/工具访问时跳过查找;overlay 不能扩大工具权限或任务范围。
适用边界 · 注意事项
- 需要测试质量审计、反模式检测或 flaky 测试调查时(用 test-anti-patterns)
- 需要运行或执行测试时(用 run-tests 技能)
- 需要从 MSTest v1/v2 升级到 v3、从 v3 升级到 v4,或需要 CI/CD 流水线配置时
- 使用 xUnit、NUnit 或 TUnit(非 MSTest)或非 .NET 代码时
- 需要从零设计新测试用例(交给 code-testing-agent)、只做报告式审计、创建项目文件而非解释 MSTest 设置、或迁移框架时