一直想学习一下单元测试 但是感觉比较复杂 而且对于小型的项目 点击的测试 基本上够用了 所以,一直就没有学 现在开始学一下
为什么现在该学单元测试?
以前 | 现在 |
---|---|
写完代码 → 手动运行控制台 → 看输出是否对 | 写完代码 → 按下Ctrl +Shift +R → 10个测试自动跑完,红/绿灯一目了然 |
改了一行代码,担心影响其他功能 → 心里打鼓 | 改了一行代码,测试全过 → 安心提交,甚至敢重构! |
没有文档,别人看不懂你写的 HelperClass | 测试就是活文档:ShouldReturnTrueWhenInputIsPositive()比注释更清晰 |
项目越做越大,不敢动老代码 | 有了测试覆盖,老代码变成“安全区”,敢改、敢优化 |
常用测试框架
常用的C#单元测试有 XUnit
MSTest
NUnit
测试框架 | 优点 | 适合你吗 |
---|---|---|
xUnit.net | 轻量、现代、性能好、约定优于配置、社区推荐度高 | 强烈推荐新手入门 |
NUnit | 功能丰富、支持 [TestCase]、老牌稳定 | 适合需要复杂参数化测试的场景 |
MSTest | VS内置、微软官方、无需安装包 | 不推荐新项目,语法老旧,灵活性差 |
单元测试的方法与规范
单元测试的黄金法则:AAA 模式(Arrange-Act-Assert)
这是所有现代单元测试的底层结构。不管你用 xUnit、NUnit 还是 MSTest,都必须遵循!
阶段 | 说明 | 示例 |
---|---|---|
Arrange(准备) | 创建被测对象、设置输入数据、模拟依赖 | var calc = new Calculator(); |
Act(执行) | 调用你要测试的方法 | var result = calc.Add(2, 3); |
Assert(断言) | 验证结果是否符合预期 | Assert.Equal(5, result); |
规范建议:
- 每个测试方法只测试一个行为
- 每个测试方法必须有且仅有一次 Act
用注释或空行分隔 AAA 三部分,提升可读性
[Fact] public void Divide_WhenDivisorIsZero_ThrowsArgumentException() { // Arrange var calculator = new Calculator(); double dividend = 10; double divisor = 0; // Act & Assert var exception = Assert.Throws<ArgumentException>(() => calculator.Divide(dividend, divisor)); // 可选:验证异常信息(更严谨) Assert.Equal("除数不能为零", exception.Message); }
测试命名规范
语义化 > 简洁
不要叫 Test1()
、TestMethod()
!
推荐命名:MethodName_State_ExpectedBehavior
场景 | 好的命名 |
---|---|
正常情况 | Add_ShouldReturnSumOfTwoPositiveNumbers |
边界值 | Divide_WhenDivisorIsZero_ThrowsArgumentException |
异常路径 | CalculateDiscount_WhenUserIsUnder18_ReturnsZero |
多参数组合 | CalculateTax_WhenIncomeIs50kAndStateIsCA_Returns7500 |
✅ 使用下划线 _ 分隔,清晰易读
✅ 用动词短语描述行为(Should / Returns / Throws)
✅ 所有单词首字母大写(PascalCase),保持风格统一
📌 xUnit 官方推荐命名风格:https://xunit.net/docs/style-guidelines
测试粒度:一个测试只测一件事
❌ 错误示范:一个测试干了三件事
[Fact]
public void ShouldWorkCorrectly()
{
var calc = new Calculator();
Assert.Equal(5, calc.Add(2, 3)); // 测试加法
Assert.Equal(6, calc.Multiply(2, 3)); // 测试乘法
Assert.True(calc.IsPositive(5)); // 测试正数判断
}
问题:
- 任何一个失败,你都不知道是哪个功能出了问题
- 无法单独运行某个子场景
难以维护
✅ 正确做法:拆成三个独立测试[Fact] public void Add_ShouldReturnSumOfTwoNumbers() { ... } [Fact] public void Multiply_ShouldReturnProductOfTwoNumbers() { ... } [Fact] public void IsPositive_ShouldReturnTrueForPositiveNumber() { ... }
避免测试中的“魔法数字”和硬编码**
❌ 不好的写法:
Assert.Equal(12345, service.CalculateTotal());
✅ 更好的写法:
const int expectedTotal = 12345;
var result = service.CalculateTotal();
Assert.Equal(expectedTotal, result);
测试数据:善用 [Theory] + [InlineData] 做参数化测试
当你需要测试多个输入输出组合时,别写 10 个重复的 [Fact]
!
✅ 示例:测试加法多种情况
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
[InlineData(999, 1, 1000)]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
var calc = new Calculator();
var result = calc.Add(a, b);
Assert.Equal(expected, result);
}
✅ 优势:
- 代码复用
- 自动显示每个用例的名称(VS测试资源管理器中会列出每组)
- 新增用例只需加一行,不改结构
✅ 六、异常测试:一定要断言异常类型 + 消息(如果重要)
[Fact]
public void Divide_WhenDivisorIsZero_ThrowsArgumentExceptionWithMessage()
{
var calc = new Calculator();
var ex = Assert.Throws<ArgumentException>(() =>
calc.Divide(10, 0));
// ✅ 不仅要抛异常,还要确保错误信息正确
Assert.Equal("除数不能为零", ex.Message);
}
测试不应依赖外部环境(数据库、文件、网络)
单元测试 = 单元测试。意思是:隔离、快速、可重复。
❌ 错误示范:
[Fact]
public void GetUserFromDatabase_ReturnsValidUser()
{
var repo = new UserRepository(); // 实际连了 SQL Server
var user = repo.GetUserById(1); // 依赖真实数据库
Assert.NotNull(user);
}
👉 问题:
- 慢(数据库连接耗时)
- 不稳定(网络断了?数据被删了?)
- 无法 CI 自动运行
✅ 正确做法:使用 Mock(模拟)
[Fact]
public void GetUserById_WhenIdExists_ReturnsUser()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(r => r.GetUserById(1))
.Returns(new User { Id = 1, Name = "Alice" });
var service = new UserService(mockRepo.Object);
// Act
var user = service.GetUserById(1);
// Assert
Assert.NotNull(user);
Assert.Equal("Alice", user.Name);
}
✅ 使用 Moq 库(NuGet 包:Moq)
✅ 你的测试只关心“接口契约”,不关心实现细节
✅ 测试速度从秒级 → 毫秒级
💡 一句话记住:单元测试不碰磁盘、不联网、不读配置文件、不启动服务
测试代码也要整洁:DRY & KISS
测试代码也是代码!它也需要:
原则 | 说明 |
---|---|
DRY(Don’t Repeat Yourself) | 相同的 Arrange 逻辑抽到 SetUp或构造函数(xUnit 用构造函数) |
KISS(Keep It Simple, Stupid) | 别在测试里写复杂逻辑、循环、条件分支 |
测试即文档(Documentation by Example)
你的测试,就是最真实的“使用说明书”。
[Fact]
public void CalculateDiscount_WhenUserIsPremiumAndHasMoreThan10Orders_Returns20Percent()
{
var user = new User { IsPremium = true, OrderCount = 15 };
var discount = service.CalculateDiscount(user);
Assert.Equal(0.20m, discount);
}
总结:单元测试十项黄金规范(可打印贴墙上)
- 每个测试只做一件事(单一职责)
- 使用 AAA 模式,结构清晰
- 命名格式:MethodName_State_ExpectedBehavior
- 用 [Theory] + [InlineData] 替代重复测试
- 测试异常时,同时断言类型和消息
- 绝不依赖外部资源(数据库、文件、网络)
- 使用 Moq 模拟依赖,保证隔离性
- 测试代码也要整洁、可读、无重复
- 覆盖率不是目标,有意义的覆盖才是
- 测试是最好的文档 —— 让别人读懂你的意图
这篇文章有通义千问的一份功劳
本文由 jxxxy 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。