You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#中内部细节关联度高的类如何正确进行单元测试?

问题解答

你暂称为“容器模式”的实现,本质是简化版工作单元(Unit of Work)模式,核心职责就是跟踪内存中待持久化的实体,批量提交到仓储完成持久化后清空跟踪状态,是很常见的设计思路。

现有测试方案的问题

单元测试的核心准则是只验证类型的公开可观测行为,不耦合内部实现细节。你目前用internal构造函数注入外部列表、直接断言列表状态的写法,本质是把“内部用List<Entity>存储实体”这个实现细节和测试强绑定了——后续如果要把内部存储改成ConcurrentQueue、分块数组等其他结构,哪怕对外Add、Save的行为完全没有变化,现有测试也会直接失败,维护成本很高。

你完全不需要暴露内部状态、也不需要反射访问私有字段,仅通过两个公开方法就能覆盖全部逻辑,完全符合单元测试规范。

正确的测试设计思路

你不需要单独“孤立测试Add方法”,Add方法本身没有对外返回值,它的可观测行为只有一个:添加的实体会在后续调用SaveAndClearAsync时被传递给仓储。同理SaveAndClearAsync的所有逻辑分支,都可以通过mock仓储、校验方法返回值、校验仓储的被调用情况来验证,不需要感知内部列表的存在。
你需要覆盖的核心测试场景如下:

  • 未添加任何实体时调用SaveAndClearAsync:不触发仓储保存逻辑,直接返回true
  • 调用Add添加实体后调用SaveAndClearAsync:仓储仅被调用一次,入参包含所有添加的实体,方法返回true
  • 仓储保存抛出异常时:方法返回false,已添加的实体不会被清空,下次调用保存时会再次传给仓储
  • 保存成功后再次调用SaveAndClearAsync:之前已保存的实体不会重复传给仓储(即内部状态已清空)

测试代码示例

首先可以删掉你写的internal测试专用构造函数,仅保留生产环境用的公有构造函数即可,测试代码如下:

// 1. 验证Add的实体确实会被持久化
[Fact]
public async Task Add_EntityAdded_PassedToRepoOnSave()
{
    // Arrange
    var repoMock = new Mock<IRepository>();
    var container = new Container(repoMock.Object);
    var testEntity = new Entity { Name = "md52" };

    // Act
    container.Add(testEntity);
    var saveResult = await container.SaveAndClearAsync();

    // Assert
    Assert.True(saveResult);
    repoMock.Verify(
        r => r.SaveAsync(It.Is<List<Entity>>(list => list.Count == 1 && list[0].Name == "md52")),
        Times.Once
    );
}

// 2. 验证空列表不触发仓储调用
[Fact]
public async Task SaveAndClearAsync_NoEntities_SkipsRepoCallAndReturnsTrue()
{
    // Arrange 用Strict模式,只要调用了未Setup的SaveAsync就会直接抛错
    var repoMock = new Mock<IRepository>(MockBehavior.Strict);
    var container = new Container(repoMock.Object);

    // Act
    var result = await container.SaveAndClearAsync();

    // Assert
    Assert.True(result);
}

// 3. 验证保存失败时不清空实体
[Fact]
public async Task SaveAndClearAsync_RepoThrows_ReturnsFalseAndKeepsEntities()
{
    // Arrange
    var repoMock = new Mock<IRepository>();
    repoMock.Setup(r => r.SaveAsync(It.IsAny<List<Entity>>()))
        .ThrowsAsync(new Exception("DB connection failed"));
    var container = new Container(repoMock.Object);
    container.Add(new Entity { Name = "test_entity" });

    // Act
    var firstSaveResult = await container.SaveAndClearAsync();
    await container.SaveAndClearAsync(); // 第二次调用验证实体未丢失

    // Assert
    Assert.False(firstSaveResult);
    repoMock.Verify(
        r => r.SaveAsync(It.Is<List<Entity>>(list => list.Count == 1 && list[0].Name == "test_entity")),
        Times.Exactly(2)
    );
}

// 4. 验证保存成功后清空实体
[Fact]
public async Task SaveAndClearAsync_SaveSucceeds_ClearsEntitiesAfterCompletion()
{
    // Arrange
    var repoMock = new Mock<IRepository>();
    var container = new Container(repoMock.Object);
    container.Add(new Entity { Name = "test_entity" });

    // Act
    var firstSaveResult = await container.SaveAndClearAsync();
    await container.SaveAndClearAsync(); // 第二次调用验证列表已空

    // Assert
    Assert.True(firstSaveResult);
    repoMock.Verify(
        r => r.SaveAsync(It.Is<List<Entity>>(list => list.Count == 1)),
        Times.Once
    );
    repoMock.Verify(
        r => r.SaveAsync(It.Is<List<Entity>>(list => list.Count == 0)),
        Times.Once
    );
}

另外提两处你代码里的笔误:

  • 接口名IRepostory拼写错误,正确应为IRepository
  • 测试代码中初始化实体的代码new Entity { Name "md52" }缺少赋值等号,正确写法是new Entity { Name = "md52" }

内容的提问来源于stack exchange,提问作者Alexander Kozachenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 05:06:29