C# 单元测试:xUnit 与 Moq 让仓库模式真正被证明
对 BookReservationService(书籍预订服务)做单元测试,用 Moq 模拟 IBookRepository 与 INotificationSender,检验"仓库模式到底值不值"。xUnit + Moq 的组合实践。
Arrange-Act-Assert:测试的结构
像科学实验一样分三段:布置条件(Arrange)、跑实验(Act)、对照假设核验结果(Assert)。三段清晰分离,测试一眼可读。命名约定:MethodName_ExpectedBehavior_Condition——ReserveBookAsync_ReturnsTrue_WhenBookIsAvailable 读起来像句子,失败报告里光看测试名就知道哪里坏了。
[Fact]
public async Task ReserveBookAsync_ReturnsTrue_WhenBookIsAvailable()
{
// Arrange
var mockRepository = new Mock<IBookRepository>();
mockRepository
.Setup(repo => repo.ReserveAsync(1, "member@example.com"))
.ReturnsAsync(true);
var mockNotifier = new Mock<INotificationSender>();
var service = new BookReservationService(
mockRepository.Object, mockNotifier.Object);
// Act
var result = await service.ReserveBookAsync(1, "member@example.com");
// Assert
Assert.True(result);
}
Verify:检查"发生过",而不只是返回值
Assert 查返回值;Verify 是 Moq 的特性,查 mock 上的某个方法是否真的被调用——当你在意的不是返回值而是副作用时有用:
mockNotifier.Verify(
n => n.SendAsync("member@example.com", It.IsAny<string>()),
Times.Once);
It.IsAny<string>() 表示"不关心消息原文,只要确认用这个收件人调用了 SendAsync"。这个测试会在服务忘记调通知器时失败——哪怕预订本身仍正确返回 true,光靠 Assert 抓不到。
失败路径:确认"不该发生的没发生"
成功路径之外同样重要:ReserveBookAsync_ReturnsFalse_WhenBookIsNotAvailable 里,除了 Assert.False(result),还断言通知从未发出:
mockNotifier.Verify(
n => n.SendAsync(It.IsAny<string>(), It.IsAny<string>()),
Times.Never);
Times.Never 正是能抓真 bug 的检查:如果后来有人把通知调用移出 if (success) 块,这个测试立刻、大声、具体地失败,而不是让 bug 静默上线、给没订到书的会员发"预订成功"邮件。
Theory + InlineData:一次编写,多组输入
几乎相同的测试方法重复多次是浪费。[Theory] 配 [InlineData] 让同一测试逻辑按数据行各跑一次:
[Theory]
[InlineData(1, "member@example.com", true)]
[InlineData(2, "another@example.com", true)]
[InlineData(999, "member@example.com", false)]
public async Task ReserveBookAsync_ReturnsExpectedResult(
int bookId, string email, bool repositoryReturns)
xUnit 把这个方法按行跑三次,各自独立报告通过/失败——而不是一个合并结果。
该测什么、不该测什么:面试常问的区分
BookReservationService 用 mock 做单元测试,如上。真正的 BookRepository(跟 EF Core 和真实数据库打交道的那层)不用这种方式测——它就是面试题"你会对仓库类做单元测试吗"的正解:不会,用 mock 没有意义,因为你要测的正是"跟真实数据库对话"的那部分;它该走集成测试(用真实或临时的测试数据库、in-memory provider),验证 SQL 是否正确、EF Core 映射是否正常、连接串是否可用。
单元测试:单类逻辑隔离测试,依赖全 mock,毫秒级,无真实数据库/网络——如"预订成功时服务是否正确调用通知器"。
集成测试:多块真实组件协同,常用真实/临时数据库,较慢,但能抓 mock 物理上抓不到的问题——错误 SQL、坏映射、连接串问题——如"ReserveAsync 是否真的更新了真实数据库里正确的行"。
实践建议
- 测试名写成句子(方法_期望_条件),失败报告零开销定位;
- 副作用类行为用
Verify(Times.Once/Never),别只断言返回值; - 数据驱动用例用
[Theory]/[InlineData],一个方法多组数据; - 仓库层不要 mock 单元测试,走集成测试——这是面试题,也是工程正确姿势。