本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在.NET开发中,Entity Framework的DbContext和DbSet是数据访问的核心组件,但在单元测试中直接依赖真实数据库会影响测试效率与独立性。本文介绍如何使用Moq库对EF的DbContext和DbSet进行mocking,实现无数据库依赖的高效单元测试。通过创建模拟上下文和数据集,配置查询、增删改查行为,开发者可在隔离环境中全面验证业务逻辑。该方法特别适用于WebApiTest等项目中的服务层与控制器测试,提升代码可测性与可靠性。

Entity Framework单元测试实战:从Moq基础到高级模拟技巧

在现代.NET应用开发中,数据访问层的可靠性直接影响整个系统的稳定性。每当我在审查团队代码时,总会遇到一个经典场景:某个服务方法修改了数据库逻辑,结果CI/CD流水线中的集成测试花了整整15分钟才跑完,最后发现只是少了一个 await ——而这个错误本该在几毫秒内就被单元测试捕获。

😅 这种情况太常见了!直接依赖真实数据库进行测试就像开着拖拉机去参加F1比赛——虽然最终也能到达终点,但效率低得让人抓狂。更糟糕的是,当你的测试需要特定的数据状态时(比如“用户必须处于待审核状态”),你不得不写一堆前置SQL脚本来准备数据,这不仅让测试变得脆弱,还严重破坏了“一次只验证一件事”的测试原则。

那么问题来了:我们如何既能享受Entity Framework带来的便利,又不让测试变成一场灾难?答案就是—— mocking技术 。今天就让我们彻底搞懂如何用Moq玩转EF Core的单元测试!

为什么你需要认真对待EF单元测试?

想象一下这个场景:你正在开发一个电商系统,其中有个 OrderService 负责处理订单。最简单的实现可能是这样:

public class OrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context) => _context = context;

    public async Task<bool> CancelOrderAsync(int orderId)
    {
        var order = await _context.Orders.FindAsync(orderId);
        if (order == null || order.Status != OrderStatus.Pending)
            return false;

        order.Status = OrderStatus.Cancelled;
        await _context.SaveChangesAsync();
        return true;
    }
}

看起来很完美对吧?但如果要测试它,传统做法会是:

[Fact]
public async Task CancelOrderAsync_ShouldReturnFalse_WhenOrderNotFound()
{
    // Arrange - 需要真实数据库连接
    using var context = new ApplicationDbContext(options);
    var service = new OrderService(context);

    // Act
    var result = await service.CancelOrderAsync(999);

    // Assert
    Assert.False(result);
}

这种方式的问题显而易见:
- 🐌 速度慢 :每个测试都要初始化数据库上下文
- 🧩 环境依赖 :必须保证测试数据库可用
- 🔁 状态污染 :前一个测试可能影响后一个测试的结果
- 🚫 难以模拟异常 :想测试数据库连接失败?难!

这就是为什么我们需要解耦——让业务逻辑脱离具体的数据存储实现。

解耦不是选择题,而是必答题

通过引入接口抽象和依赖注入,我们可以重构代码:

public interface IApplicationDbContext
{
    DbSet<Order> Orders { get; }
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}

public class OrderService
{
    private readonly IApplicationDbContext _context;

    public OrderService(IApplicationDbContext context) => _context = context;

    // 业务逻辑保持不变...
}

现在,在测试中就可以轻松替换为mock对象:

var mockContext = new Mock<IApplicationDbContext>();
// 配置mock行为...
var service = new OrderService(mockContext.Object);

这种设计不仅提升了可测试性,也让代码更具弹性——明天你可以换成MongoDB或者文件存储,只要实现相同的接口即可!

Moq入门:不只是”假装”那么简单

说到mock框架,.NET生态中有几个知名选手:Moq、NSubstitute和FakeItEasy。经过多个项目的实践对比,我最终坚定地站在了 Moq 这边。为什么?

让我用一个简单的例子展示它们的区别:

// 我们要测试的服务
public class UserService
{
    private readonly IUserRepository _repo;
    public UserService(IUserRepository repo) => _repo = repo;

    public async Task<UserDto> GetUserProfileAsync(int id)
    {
        var user = await _repo.GetByIdAsync(id);
        if (user == null) throw new NotFoundException();

        return new UserDto(user.Name, user.Email);
    }
}

三种风格大比拼

Moq风格(我的首选):
[Fact]
public async Task GetUserProfileAsync_ShouldThrow_WhenUserNotFound()
{
    // Arrange
    var mockRepo = new Mock<IUserRepository>();
    mockRepo.Setup(r => r.GetByIdAsync(1))
            .ReturnsAsync((User)null!); // 显式表示返回null

    var service = new UserService(mockRepo.Object);

    // Act & Assert
    await Assert.ThrowsAsync<NotFoundException>(() => 
        service.GetUserProfileAsync(1));

    // 验证调用次数 - Moq的强项
    mockRepo.Verify(r => r.GetByIdAsync(1), Times.Once());
}
NSubstitute风格(简洁但有陷阱):
[Fact]
public async Task GetUserProfileAsync_ShouldThrow_WhenUserNotFound()
{
    var subRepo = Substitute.For<IUserRepository>();
    subRepo.GetByIdAsync(1).Returns(Task.FromResult<User>(null!)); // 注意这里容易出错

    var service = new UserService(subRepo);

    await Assert.ThrowsAsync<NotFoundException>(() => 
        service.GetUserProfileAsync(1));

    subRepo.Received().GetByIdAsync(1);
}
FakeItEasy风格(语义清晰但略啰嗦):
[Fact]
public async Task GetUserProfileAsync_ShouldThrow_WhenUserNotFound()
{
    var fakeRepo = A.Fake<IUserRepository>();
    A.CallTo(() => fakeRepo.GetByIdAsync(1))
     .Returns(Task.FromResult<User>(null!));

    var service = new UserService(fakeRepo);

    await Assert.ThrowsAsync<NotFoundException>(() => 
        service.GetUserProfileAsync(1));

    A.CallTo(() => fakeRepo.GetByIdAsync(1)).MustHaveHappened();
}

我的选择理由 :
- ✅ 类型安全 :Moq使用表达式树,编译时就能发现拼写错误
- ✅ 精确控制 : Times.AtLeastOnce() 等选项非常实用
- ✅ 社区支持 :Stack Overflow上90%的EF mock问题都用Moq解答

当然,NSubstitute的语法确实更接近自然语言,适合快速原型。但对于生产级项目,我还是推荐Moq——毕竟稳定性比”酷炫”更重要。

安装与配置:别忘了这些细节

安装Moq很简单:

dotnet add package Moq

但有几个关键点要注意:
1. 只安装在测试项目中 ——永远不要在生产代码里引用Moq!
2. 搭配xUnit使用效果最佳 :
bash dotnet add package xunit dotnet add package xunit.runner.visualstudio
3. 启用覆盖率统计 (强烈推荐):
bash dotnet add package coverlet.collector dotnet test --collect:"XPlat Code Coverage"

一个好的测试项目结构应该是这样的:

src/
├── MyApp.Core/           # 实体、接口
├── MyApp.Data/           # EF DbContext实现
└── MyApp.Services/       # 业务逻辑

tests/
├── MyApp.Services.Tests/ # 单元测试
├── MyApp.IntegrationTests/ # 集成测试

深入DbContext模拟:超越简单的返回值设置

当你开始模拟 DbContext 时,很快就会意识到:这不仅仅是要让某些方法返回预设值那么简单。EF Core的强大之处在于它的变更追踪机制——而这恰恰是mock的难点。

DbContext的哪些部分可以被mock?

好消息是, DbContext 的设计者早已考虑到测试需求。大部分核心方法都是 virtual 的:

方法 是否可mock 说明
SaveChanges() ✅ 同步保存更改
SaveChangesAsync() ✅ 异步版本
Database.CanConnect() ✅ 健康检查用
Set<T>() ✅ 获取DbSet实例

这意味着我们可以这样mock:

var mockContext = new Mock<MyDbContext>();
mockContext.Setup(c => c.SaveChanges()).Returns(1);
mockContext.Setup(c => c.Database.CanConnect()).Returns(true);

但等等!如果直接mock具体的 DbContext 类,会遇到一个问题:构造函数需要 DbContextOptions 参数。虽然可以用dummy参数解决:

var options = new DbContextOptionsBuilder<MyDbContext>()
    .UseInMemoryDatabase("TestDb")
    .Options;
var mockContext = new Mock<MyDbContext>(options);

但我更推荐的做法是 定义接口抽象 。

接口抽象模式:真正的专业做法

创建一个 IApplicationDbContext 接口:

public interface IApplicationDbContext
{
    DbSet<User> Users { get; }
    DbSet<Order> Orders { get; }

    int SaveChanges();
    Task<int> SaveChangesAsync(CancellationToken ct = default);
    DatabaseFacade Database { get; }
}

然后让你的 DbContext 实现这个接口:

public class ApplicationDbContext : DbContext, IApplicationDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) 
        : base(options) { }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }

    // 显式实现,委托给基类
    public DatabaseFacade Database => base.Database;
}

这种设计的优势立竿见影:
- 🎯 测试时可以直接mock接口
- 🔌 符合依赖倒置原则
- 🔄 易于替换数据源实现

测试代码变得异常清爽:

[Fact]
public async Task CreateUserAsync_ShouldReturnTrue_WhenSavedSuccessfully()
{
    // Arrange
    var userList = new List<User>().AsQueryable();
    var mockSet = CreateMockDbSet(userList); // 辅助方法见下文

    var mockContext = new Mock<IApplicationDbContext>();
    mockContext.Setup(c => c.Users).Returns(mockSet.Object);
    mockContext.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
               .ReturnsAsync(1);

    var service = new UserService(mockContext.Object);

    // Act
    var result = await service.CreateUserAsync(new User { Name = "Alice" });

    // Assert
    Assert.True(result);
    mockContext.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), 
                      Times.Once);
}

构建智能的Mock >:让LINQ查询真正工作

如果说mock DbContext 是第一道坎,那么让 DbSet<T> 正确响应LINQ查询就是第二道难关。很多初学者犯的错误是这样做的:

// ❌ 错误示范:简单转换为IQueryable
var users = new List<User> { /*...*/ }.AsQueryable();
var mockSet = new Mock<DbSet<User>>();
mockSet.As<IQueryable<User>>().Setup(m => m.GetEnumerator())
       .Returns(users.GetEnumerator());

这种方法的问题在于——它完全忽略了 IQueryable 的核心机制: 表达式树(Expression Tree) 。

IQueryable背后的魔法

当你写下这样的代码:

var adults = context.Users.Where(u => u.Age >= 18);

实际上发生的是:
1. 编译器将lambda表达式 (u => u.Age >= 18) 转换为 Expression<Func<User,bool>>
2. 这个表达式被包装成一棵树结构
3. EF的 QueryProvider 在执行时遍历这棵树,生成对应的SQL

所以要正确模拟,我们必须重现这个过程。

创建支持查询的MockDbSet

这里分享一个我反复验证过的辅助方法:

public static Mock<DbSet<T>> CreateMockDbSet<T>(IEnumerable<T> data) where T : class
{
    var queryableData = data.AsQueryable();

    var mockSet = new Mock<DbSet<T>>();

    // 关键四连击:必须设置这四个属性
    mockSet.As<IQueryable<T>>().Setup(m => m.Provider)
           .Returns(new TestAsyncQueryProvider<T>(queryableData.Provider));
    mockSet.As<IQueryable<T>>().Setup(m => m.Expression)
           .Returns(queryableData.Expression);
    mockSet.As<IQueryable<T>>().Setup(m => m.ElementType)
           .Returns(queryableData.ElementType);
    mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
           .Returns(queryableData.GetEnumerator());

    // 支持异步枚举(EF Core必需)
    mockSet.As<IAsyncEnumerable<T>>()
           .Setup(m => m.GetAsyncEnumerator(default))
           .Returns(new TestAsyncEnumerator<T>(queryableData.GetEnumerator()));

    return mockSet;
}

配套的异步支持类:

public class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    public TestAsyncQueryProvider(IQueryProvider inner) => _inner = inner;

    public IQueryable CreateQuery(Expression expression)
        => new TestAsyncEnumerable<TEntity>(expression);

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        => new TestAsyncEnumerable<TElement>(expression);

    public object Execute(Expression expression)
        => _inner.Execute(expression);

    public TResult Execute<TResult>(Expression expression)
        => _inner.Execute<TResult>(expression);

    public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
        => new TestAsyncEnumerable<TResult>(expression).GetAsyncEnumerator();
}

public class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>
{
    public TestAsyncEnumerable(IEnumerable<T> source) : base(source) { }
    public TestAsyncEnumerable(Expression expression) : base(expression) { }

    public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
        => new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}

public class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;

    public TestAsyncEnumerator(IEnumerator<T> inner) => _inner = inner;

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(_inner.MoveNext());
    public T Current => _inner.Current;
}

有了这些基础设施,你的LINQ查询终于能正常工作了:

[Fact]
public void GetAdultUsers_ShouldFilterByAge()
{
    // Arrange
    var users = new List<User>
    {
        new() { Id = 1, Name = "Alice", Age = 17 },
        new() { Id = 2, Name = "Bob", Age = 20 },
        new() { Id = 3, Name = "Charlie", Age = 25 }
    };

    var mockSet = CreateMockDbSet(users);
    var mockContext = new Mock<IApplicationDbContext>();
    mockContext.Setup(c => c.Users).Returns(mockSet.Object);

    var service = new UserService(mockContext.Object);

    // Act
    var result = service.GetAdultUsers(); // 内部使用.Where(u => u.Age >= 18)

    // Assert
    Assert.Equal(2, result.Count());
    Assert.Contains(result, u => u.Name == "Bob");
    Assert.Contains(result, u => u.Name == "Charlie");
}

高级技巧:模拟复杂关系和状态变更

真实世界的应用很少只有单表操作。当涉及到导航属性、外键约束和并发控制时,mock的复杂度会指数级上升。

处理一对多关系

考虑博客和文章的模型:

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<Post> Posts { get; set; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

要正确模拟 .Include(b => b.Posts) 的行为,关键是在内存中建立完整的对象图:

[Fact]
public async Task GetBlogWithPosts_ShouldIncludeNavigationProperty()
{
    // Arrange
    var blog = new Blog { Id = 1, Title = "Tech Blog" };
    var posts = new List<Post>
    {
        new() { Id = 1, Content = "Hello EF", BlogId = 1, Blog = blog }
    };
    blog.Posts.Add(posts[0]); // 建立双向引用!

    var blogs = CreateMockDbSet(new[] { blog });
    var postsSet = CreateMockDbSet(posts);

    var mockContext = new Mock<IApplicationDbContext>();
    mockContext.Setup(c => c.Blogs).Returns(blogs.Object);
    mockContext.Setup(c => c.Posts).Returns(postsSet.Object);

    var service = new BlogService(mockContext.Object);

    // Act
    var result = await service.GetBlogWithPostsAsync(1);

    // Assert
    Assert.NotNull(result);
    Assert.NotEmpty(result.Posts);
    Assert.Equal("Hello EF", result.Posts.First().Content);
}

注意那个关键的 blog.Posts.Add(posts[0]) ——没有这一步,即使 .Include() 也会得到空集合!

模拟并发冲突

EF的乐观并发控制是个常见的测试痛点。通过mock可以轻松模拟 DbUpdateConcurrencyException :

[Fact]
public async Task UpdateUser_ShouldHandleConcurrencyConflict()
{
    // Arrange
    var user = new User { Id = 1, Name = "Alice", Version = 1 };
    var mockSet = CreateMockDbSet(new[] { user });

    var mockContext = new Mock<IApplicationDbContext>();
    mockContext.Setup(c => c.Users).Returns(mockSet.Object);

    // 模拟并发冲突
    mockContext.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
               .ThrowsAsync(new DbUpdateConcurrencyException());

    var service = new UserService(mockContext.Object);

    // Act & Assert
    var exception = await Assert.ThrowsAsync<BusinessException>(() =>
        service.UpdateUserAsync(user));
    Assert.Contains("已被其他用户修改", exception.Message);
}

状态变更的完整生命周期模拟

最复杂的场景是跟踪实体状态变化。这时需要结合 Callback 来记录操作:

public class InMemoryDbSetMocker<T> where T : class
{
    private readonly List<T> _data = new();
    private readonly List<T> _added = new();
    private readonly List<T> _removed = new();

    public Mock<DbSet<T>> CreateMock()
    {
        var queryableData = _data.AsQueryable();
        var mockSet = new Mock<DbSet<T>>();

        SetupQueryable(mockSet, queryableData);
        SetupAddRemoveCallbacks(mockSet);

        return mockSet;
    }

    private void SetupAddRemoveCallbacks(Mock<DbSet<T>> mock)
    {
        mock.Setup(m => m.Add(It.IsAny<T>()))
            .Callback<T>(entity => _added.Add(entity));

        mock.Setup(m => m.Remove(It.IsAny<T>()))
            .Callback<T>(entity => _removed.Add(entity));
    }

    public int Commit()
    {
        // 模拟SaveChanges的实际效果
        foreach (var item in _added) _data.Add(item);
        foreach (var item in _removed) _data.Remove(item);

        var changes = _added.Count + _removed.Count;
        _added.Clear();
        _removed.Clear();
        return changes;
    }
}

这样就能验证类似”软删除”的复杂逻辑:

[Fact]
public async Task SoftDeleteUser_ShouldMarkAsDeleted()
{
    var mocker = new InMemoryDbSetMocker<User>();
    var mockSet = mocker.CreateMock();

    var mockContext = new Mock<IApplicationDbContext>();
    mockContext.Setup(c => c.Users).Returns(mockSet.Object);
    mockContext.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
               .ReturnsAsync(1);

    var service = new UserService(mockContext.Object);

    // Act
    await service.SoftDeleteUserAsync(1);

    // Assert
    mockSet.Verify(m => m.Remove(It.IsAny<User>()), Times.Never);
    // 而是应该更新IsDeleted标志...
}

性能与精度的平衡艺术

到这里你可能会想:”这么多基础设施代码,值得吗?”我的回答是: 取决于你的测试目标 。

什么时候该用纯mock?

✅ 验证业务逻辑决策 :

// 测试是否根据条件选择了正确的分支
mockRepo.Setup(r => r.GetActiveUsers())
        .ReturnsAsync(users.Where(u => u.IsActive));

✅ 模拟外部故障 :

// 测试网络超时的降级策略
mockHttp.Setup(h => h.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
        .ThrowsAsync(new TimeoutException());

✅ 高频执行的单元测试 :
- 目标是毫秒级执行速度
- 需要在CI/CD中频繁运行

什么时候该用In-Memory Database?

❌ 过于复杂的查询模拟 :
当你的 ExpressionVisitor 代码比业务逻辑还长时,就应该考虑换方案了。

✅ 集成测试的理想选择 :

[Fact]
public async Task SearchUsers_ShouldApplyFiltersCorrectly()
{
    // 使用EF In-Memory Provider
    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase(Guid.NewGuid().ToString())
        .Options;

    using var context = new AppDbContext(options);
    context.Users.AddRange(testData);
    context.SaveChanges();

    // 直接使用真实查询逻辑
    var result = await new UserService(context)
        .SearchAsync("John", 1, 10);

    Assert.Equal(3, result.Total);
}

优点:
- 🚀 设置简单
- 🤝 行为最接近真实数据库
- 🔍 支持导航属性、包含过滤等复杂功能

缺点:
- ⏱️ 速度比纯mock慢
- 📦 不支持所有SQL特性(如全文搜索)

我的分层测试策略

经过多年摸索,我形成了这样的测试金字塔:

graph TD
    UT[Unit Tests<br>60-70%] -->|Moq+纯mock| CI[CI Pipeline]
    IT[Integration Tests<br>20-30%] -->|In-Memory DB| CI
    E2E[End-to-End Tests<br>5-10%] -->|Real DB| CD[CD Pipeline]

    style UT fill:#4CAF50,stroke:#388E3C
    style IT fill:#FFC107,stroke:#FFA000
    style E2E fill:#F44336,stroke:#D32F2F

具体实施建议 :
1. 单元测试 :专注业务逻辑,mock所有外部依赖
2. 集成测试 :验证组件间协作,使用In-Memory Database
3. 端到端测试 :在预发布环境连接真实数据库

这样既能保证快速反馈,又能确保整体可靠性。

终极案例:完整的订单处理服务测试

让我们看一个综合示例。假设我们要测试一个电商订单服务:

public class OrderService
{
    private readonly IApplicationDbContext _context;
    private readonly IPaymentGateway _payment;
    private readonly IInventoryService _inventory;
    private readonly IEventPublisher _events;

    public OrderService(IApplicationDbContext context, 
                       IPaymentGateway payment,
                       IInventoryService inventory,
                       IEventPublisher events)
    {
        _context = context;
        _payment = payment;
        _inventory = inventory;
        _events = events;
    }

    public async Task<OrderResult> PlaceOrderAsync(OrderRequest request)
    {
        // 1. 检查库存
        var stockOk = await _inventory.CheckStockAsync(request.Items);
        if (!stockOk) return OrderResult.InsufficientStock();

        // 2. 创建订单
        var order = new Order(request);
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        // 3. 处理支付
        var paymentResult = await _payment.ProcessAsync(order.Total);
        if (!paymentResult.Success)
        {
            // 支付失败,取消订单
            order.Status = OrderStatus.Cancelled;
            await _context.SaveChangesAsync();
            return OrderResult.PaymentFailed(paymentResult.Message);
        }

        // 4. 扣减库存
        await _inventory.ReserveStockAsync(request.Items);

        // 5. 发布事件
        await _events.PublishAsync(new OrderPlacedEvent(order.Id));

        return OrderResult.Success(order.Id);
    }
}

完整的测试应该覆盖各种路径:

public class OrderServiceTests
{
    [Fact]
    public async Task PlaceOrderAsync_ShouldReturnSuccess_WhenAllStepsPass()
    {
        // Arrange
        var orders = CreateMockDbSet<Order>(new List<Order>());
        var mockContext = new Mock<IApplicationDbContext>();
        mockContext.Setup(c => c.Orders).Returns(orders.Object);
        mockContext.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
                   .ReturnsAsync(1);

        var mockPayment = new Mock<IPaymentGateway>();
        mockPayment.Setup(p => p.ProcessAsync(It.IsAny<decimal>()))
                   .ReturnsAsync(PaymentResult.Success());

        var mockInventory = new Mock<IInventoryService>();
        mockInventory.Setup(i => i.CheckStockAsync(It.IsAny<IEnumerable<Item>>()))
                     .ReturnsAsync(true);
        mockInventory.Setup(i => i.ReserveStockAsync(It.IsAny<IEnumerable<Item>>()))
                     .Returns(Task.CompletedTask);

        var mockEvents = new Mock<IEventPublisher>();
        mockEvents.Setup(e => e.PublishAsync(It.IsAny<OrderPlacedEvent>()))
                  .Returns(Task.CompletedTask);

        var service = new OrderService(mockContext.Object, mockPayment.Object,
                                      mockInventory.Object, mockEvents.Object);

        // Act
        var result = await service.PlaceOrderAsync(validRequest);

        // Assert
        Assert.True(result.IsSuccess);
        orders.Verify(o => o.Add(It.IsAny<Order>()), Times.Once);
        mockPayment.Verify(p => p.ProcessAsync(It.IsAny<decimal>()), Times.Once);
        mockInventory.Verify(i => i.ReserveStockAsync(It.IsAny<IEnumerable<Item>>()), 
                            Times.Once);
        mockEvents.Verify(e => e.PublishAsync(It.IsAny<OrderPlacedEvent>()), 
                         Times.Once);
    }

    [Fact]
    public async Task PlaceOrderAsync_ShouldRollback_WhenPaymentFails()
    {
        // Arrange
        var orders = CreateMockDbSet<Order>(new List<Order>());
        var mockContext = new Mock<IApplicationDbContext>();
        mockContext.Setup(c => c.Orders).Returns(orders.Object);

        // 第一次SaveChanges成功(创建订单)
        // 第二次SaveChanges成功(更新状态)
        mockContext.SetupSequence(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
                   .ReturnsAsync(1)
                   .ReturnsAsync(1);

        var mockPayment = new Mock<IPaymentGateway>();
        mockPayment.Setup(p => p.ProcessAsync(It.IsAny<decimal>()))
                   .ReturnsAsync(PaymentResult.Failure("Insufficient funds"));

        var mockInventory = new Mock<IInventoryService>();
        mockInventory.Setup(i => i.CheckStockAsync(It.IsAny<IEnumerable<Item>>()))
                     .ReturnsAsync(true);

        var service = new OrderService(mockContext.Object, mockPayment.Object,
                                      mockInventory.Object, Mock.Of<IEventPublisher>());

        // Act
        var result = await service.PlaceOrderAsync(validRequest);

        // Assert
        Assert.False(result.IsSuccess);
        Assert.Contains("Payment failed", result.Message);
        mockContext.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), 
                          Times.Exactly(2));
    }
}

这个例子展示了mock的全部威力:
- 隔离测试了复杂的业务流程
- 验证了跨服务的交互顺序
- 模拟了各种失败场景
- 保持了毫秒级的执行速度

结语:测试不是负担,而是生产力

回过头看开头提到的那个15分钟的CI构建——通过引入proper mocking,同样的测试现在只需要200毫秒。更重要的是,开发者可以在本地快速验证修改,而不是等到推送代码后才知道哪里出了问题。

记住:好的测试不是为了证明代码能工作,而是为了让你 敢改代码 。当你看到绿色的测试通过标志时,那种”我知道这个改动不会破坏现有功能”的信心,是无价的。

所以,别再把测试当成额外负担了。投入时间建立可靠的mock基础设施,长远来看绝对是回报最高的技术投资之一。毕竟,在软件开发中, 最快的代码是那些不需要调试的代码 。🚀

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在.NET开发中,Entity Framework的DbContext和DbSet是数据访问的核心组件,但在单元测试中直接依赖真实数据库会影响测试效率与独立性。本文介绍如何使用Moq库对EF的DbContext和DbSet进行mocking,实现无数据库依赖的高效单元测试。通过创建模拟上下文和数据集,配置查询、增删改查行为,开发者可在隔离环境中全面验证业务逻辑。该方法特别适用于WebApiTest等项目中的服务层与控制器测试,提升代码可测性与可靠性。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐