写 C# 的团队常卡在同一处:业务代码能跑,但没人敢改,一改就怕线上崩,想补测试又不知道从哪下手,UI 测试更是写完三天就红一片。这篇按落地顺序给一套做法——先定测什么、再选框架和目录结构,然后解决外部依赖和 UI 稳定性这两个最容易翻车的地方,最后接进流水线。
写 C# 的团队常卡在同一个地方:业务代码能跑,但没人敢改,一改就怕线上崩。想补测试又不知道从哪下手,UI 自动化更是写完三天就红一片,最后没人看。这篇按落地顺序给一套做法——先定测什么,再选框架和目录结构,然后解决外部依赖和 UI 稳定性这两个最容易翻车的地方,最后接进流水线。
前提说清楚:下面的方案适合已经有稳定迭代节奏的 C# 项目。如果还在原型验证阶段,或者写完下周就下线的临时脚本,硬补测试反而拖慢节奏,先跳过这一轮。
先想清楚测什么:别一上来就追求覆盖率
覆盖率是个结果指标,不是起点。一上来盯着数字,结果就是测了一堆 getter/setter 和纯转发层,真正容易出事的地方反而没人碰。按风险排序更实用:分支多、判断逻辑绕、出错了影响面大的地方先测。
| 模块类型 | 优先测吗 | 原因 |
|---|---|---|
| 纯逻辑计算、金额与权限判断、状态机 | 优先 | 分支多,改一处容易牵动别处 |
| 数据转换、格式校验 | 优先 | 边界条件密集,手工验证成本高 |
| 接口转发、透明代理类 | 可以后放 | 本身没什么判断,测了收益低 |
| 依赖外部接口/数据库/时钟/随机数 | 先隔离再测 | 不能隔离就没法稳定重复跑 |
落地动作很具体:拿一张表列出模块、风险等级、有没有外部依赖,然后排一个两周的补测顺序。只产出一张表,不写代码,先把顺序定下来。
选框架和目录结构:一次定好,后面少折腾
C# 里主流是 xUnit、NUnit、MSTest 三家,差别主要在断言写法、并行执行的默认行为、参数化测试的表达方式。没有哪家绝对更好,团队熟悉哪个就用哪个,换框架的收益通常抵不上迁移成本。
- xUnit
- 默认并行跑测试类,断言风格偏简洁,新项目用得较多
- NUnit
- 特性体系完整,参数化和分类标记的表达比较直观
- MSTest
- 和 Visual Studio 及微软工具链贴合度高,老项目常见
- 测试项目单独建工程,命名和被测工程对应,确保测试代码不会被打进发布产物
- 测试类按「一个类对应一个被测类」切分,别把所有测试塞进一个大文件
- 方法名写清前提条件和期望结果,看名字就知道这条挂了意味着什么
- 同一逻辑多组入参优先用参数化特性,别复制十份几乎一样的测试方法
- 目录按被测代码的命名空间镜像,找起来不用猜
项目规模大、多个测试工程并存时,还要提前定好测试代码的引用方式:被测逻辑尽量抽到可被独立引用的类库里,避免测试工程反向依赖可执行程序。这一步没做好,后期会出现测试跑不起来、循环依赖的麻烦。
把外部依赖隔开:单元测试跑得快才有人愿意跑
单元测试的价值一半在速度。要跑两分钟以上,本地就没人在提交前执行,等于白写。而拖慢速度的通常就是真实数据库、真实 HTTP 调用、真实文件读写、系统时钟和随机数。
- 找出所有外部接触点把数据库、HTTP、文件、时钟、随机数全部抽象成接口,在业务代码里只依赖接口,不直接 new 具体实现。
- 准备替身简单场景手写一个假实现就够,行为复杂的用 mock 框架。替换目标是让测试里的时间、返回值完全可控。
- 分离集成测试依赖真实环境的用例单独放一个工程或单独打标签,和单元测试分开跑,前者慢且需要环境,后者应是毫秒级。
- 验证隔离效果整个单元测试套件跑完超过一两分钟,基本说明隔离没做干净,回头继续拆。
判断标准很简单:断网之后单元测试还能全绿,隔离就算做到位了。做不到说明还有地方在偷偷连外部。
UI 自动化:先解决不稳定,再谈覆盖面
UI 测试最容易死在两个地方:元素定位太脆,和等待方式不对。界面一改坐标和层级路径就全断,固定 sleep 又会让整套测试又慢又随机。这两个问题不解决,覆盖多少用例都是负担。
| 做法 | 问题 | 改成什么 |
|---|---|---|
| 按坐标或层级路径定位 | 界面微调就失效 | 用 AutomationId、可访问性名称等稳定标识 |
| 固定 sleep 等待 | 快机器上浪费、慢机器上仍失败 | 等条件成立,并给每个操作设超时上限 |
| UI 覆盖所有分支 | 维护成本吃掉收益 | UI 只测主流程和关键分支 |
| 超时后无证据 | 失败无法复现 | 超时自动截图并保留当时的页面状态 |
排查顺序建议固定下来:先确认是元素找不到、还是页面没加载完、还是环境不一致(分辨率、驱动版本、登录态)。三类原因的处理方式完全不同,混着查会浪费大量时间。
- 先在本地固定环境跑一遍,确认不是环境差异导致的失败
- 再看失败截图,定位是元素缺失还是渲染未完成
- 检查选择器是否还能在页面上唯一命中
- 确认登录态和数据准备是否正确
- 连续两次无故失败就单独拿出来查,别靠重跑掩盖
UI 用例的数量控制在主流程和关键分支就够了,回归细节交给单元测试和接口测试。想了解自动化工具的分类和适用场景,可以到 /tools/speed-test 对照着选,但工具只是载体,稳定度靠的是上面这套写法。
接进流水线:让测试结果真的能拦住问题
测试写完了不进流水线,等于只有写的人知道。关键是把不同层级的测试放在不同触发点上,别让整套都堆在每次提交。
- 提交时跑单元测试,反馈要在几分钟内出来
- 合并前跑单元测试加接口测试
- UI 测试按天或按发布节点跑,不必每次提交都跑
- 不稳定用例先隔离进单独队列并记录,不要靠重跑掩盖
- 失败要带可复现信息:随机种子、输入数据、失败截图或日志片段
判断一条失败报错有没有价值,看它能不能让另一个人在不问原作者的情况下复现。缺随机种子、输入数据、截图的报错,等于没报。
如果项目打算接模型做代码审查或自动生成单测,可以参考站内 AI 代码审查和单测生成那几篇的分工来做,但生成的用例必须人工过一遍边界条件。生成结果适合当草稿,不适合直接合并。
另外,如果测试里会调用外部 API,成本和稳定性都要提前评估。各家官方 API 的当前价格和限制到 /price-watch 核对当时的数字,不要凭记忆估算,价格调整后测试预算会跟着变。
下一步按大纲把补测顺序表列出来,再挑一个风险最高的模块先跑通单元测试,最后接进流水线。想先了解自动化工具选型,可以到工具目录对照适用场景。→常见问题
C# 单元测试框架选哪个,会互相影响吗?
xUnit、NUnit、MSTest 都能满足日常需求,差异主要在断言风格、参数化写法和默认并行策略。同一个解决方案里最好统一成一个,混用会让配置和运行命令变复杂。团队没历史包袱就选社区资料最多的那个,后面换成本也不低。
UI 自动化测试总是不稳定,是不是干脆别做?
不是别做,是要缩小范围。把 UI 用例压到登录、下单、支付这类主流程,其余的用单元测试和接口测试覆盖。剩下的不稳定大多来自固定等待和脆弱定位,换成条件等待加稳定标识后能明显改善。仍偶发失败的用例单独隔离,别让它污染整体结果。
小团队没时间写测试,最低限度该做什么?
先给最容易出错、改动最频繁的那几个函数补测试,尤其是金额计算、权限判断、状态流转这类逻辑。再给每个 bug 修复补一条回归用例,防止同一个问题复发。这两件事投入不大,但能挡住大部分线上事故,比追求覆盖率数字实在。