Mocking EF DbContext与DbSet单元测试实战
简介:在.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基础设施,长远来看绝对是回报最高的技术投资之一。毕竟,在软件开发中, 最快的代码是那些不需要调试的代码 。🚀
简介:在.NET开发中,Entity Framework的DbContext和DbSet是数据访问的核心组件,但在单元测试中直接依赖真实数据库会影响测试效率与独立性。本文介绍如何使用Moq库对EF的DbContext和DbSet进行mocking,实现无数据库依赖的高效单元测试。通过创建模拟上下文和数据集,配置查询、增删改查行为,开发者可在隔离环境中全面验证业务逻辑。该方法特别适用于WebApiTest等项目中的服务层与控制器测试,提升代码可测性与可靠性。
更多推荐
所有评论(0)