编程 C# 单元测试:xUnit 与 Moq 让仓库模式真正被证明

2026-09-07 23:12:38

C# 单元测试:xUnit 与 Moq 让仓库模式真正被证明

BookReservationService(书籍预订服务)做单元测试,用 Moq 模拟 IBookRepositoryINotificationSender,检验"仓库模式到底值不值"。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 单元测试,走集成测试——这是面试题,也是工程正确姿势。

来源:Unit Testing in C# with xUnit and Moq: Proving the Repository Pattern Actually Pays Off - DEV Community

复制全文 生成海报 C# 测试 .NET

推荐文章

程序员茄子在线接单