你是不是也有这种情况:想给 Service 层写个单元测试,结果发现要连数据库、要调第三方 API、要读配置文件……折腾半天,测试没写出来,环境先崩了。
这不是你的问题,是被测代码依赖太多外部资源的问题。单元测试要的是隔离——只测当前类的逻辑,不碰任何真实的外部依赖。
Moq 就干一件事: 给被测类造一个"替身",把外部依赖全部替换掉,让你想让它返回什么就返回什么,想让它抛异常它就抛异常。
Moq 是什么
Moq(发音同"mock-you")是 .NET 平台上最常用的 Mock 框架,一个开源库。它能在运行时动态生成一个"假对象"来替代真实的依赖,让你在测试中完全控制这个假对象的行为。
为什么不用手写假对象?
假设你要测一个 UserService,它依赖 IUserRepository。不用 Moq 的话,你得手动写一个类实现 IUserRepository 接口,每个方法都要写一遍"返回什么"。测试多了,项目里全是这种"空壳类",重复劳动,维护成本还高。
Moq 帮你自动生成这些"空壳类",几行代码就能搞定。
官方资源:
GitHub:
https://github.com/moq/moq
怎么引入
测试项目中安装对应的 NuGet 包即可:
Moq 不需要在 Program.cs 中做任何配置——它只在测试项目中使用,不影响运行时代码。
快速上手
示例一:用假数据替代真实数据库查询
这是 Moq 最基础的用途:让依赖返回你指定的数据,避免真的去查数据库。
public int Id { get; set; } public string Name { get; set; }public interface IUserRepository private readonly IUserRepository _repo; public UserService(IUserRepository repo) => _repo = repo; public string GetUserDisplayName(int id) var user = _repo.GetById(id); return user == null ? "未找到" : user.Name;
测试代码:
public void GetUserDisplayName_用户存在_返回用户名() var mockRepo = new Mock<IUserRepository>(); // 2. 安排替身行为:当 GetById(1) 被调用时,返回一个 User 对象 mockRepo.Setup(r => r.GetById(1)) .Returns(new User { Id = 1, Name = "张三" }); var service = new UserService(mockRepo.Object); var result = service.GetUserDisplayName(1); // 5. 验证结果——这里返回的是"张三",而不是真的去数据库查出来的 Assert.Equal("张三", result);
关键点: 被测代码一行没改,_repo.GetById(id) 在测试环境里被 Moq 接管了,返回你指定的值。上线后它该查数据库还是查数据库。
示例二:模拟数据库超时,验证代码会不会崩
这个场景在真实项目里很难复现——你总不能真把数据库弄挂。Moq 可以轻松模拟。
public interface IUserRepository Task<User> GetUserAsync(int id); public int Id { get; set; } public string Name { get; set; }public class UserProfileService private readonly IUserRepository _repo; private readonly ILogger<UserProfileService> _logger; public UserProfileService(IUserRepository repo, ILogger<UserProfileService> logger) public async Task<string> GetDisplayNameAsync(int id) var user = await _repo.GetUserAsync(id); return user == null ? "未找到" : user.Name; catch (TimeoutException ex) _logger.LogError(ex, "数据库查询超时");
测试"数据库挂了"的场景:
public async Task GetDisplayNameAsync_数据库超时_返回友好提示() var mockRepo = new Mock<IUserRepository>(); var mockLogger = new Mock<ILogger<UserProfileService>>(); // 让 GetUserAsync 抛出超时异常,模拟数据库挂了 mockRepo.Setup(r => r.GetUserAsync(1)) .ThrowsAsync(new TimeoutException("数据库连接超时")); var service = new UserProfileService(mockRepo.Object, mockLogger.Object); var result = await service.GetDisplayNameAsync(1); Assert.Equal("系统繁忙,请稍后重试", result); // 注意:LogError 是 ILogger 的扩展方法,不能直接用 Moq 的 Verify 去验证 It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("数据库查询超时")), It.IsAny<TimeoutException>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()),
这里有个新手极易踩的坑:LogError 是 ILogger 的扩展方法,不是接口方法。直接用 Verify(l => l.LogError(...)) 会报 NotSupportedException。必须验证底层的 Log 方法。
示例三:验证方法有没有被正确调用
有些方法没有返回值,只负责"保存"或"发消息"。这时候需要 Moq 来验证行为。
public int Id { get; set; } public string Status { get; set; }public interface IOrderRepositorypublic class OrderService private readonly IOrderRepository _repo; public OrderService(IOrderRepository repo) => _repo = repo; public void CancelOrder(int orderId) var order = _repo.GetById(orderId); if (order != null && order.Status == "待付款")public void CancelOrder_待付款订单_调用Save保存取消状态() var mockRepo = new Mock<IOrderRepository>(); var order = new Order { Id = 1, Status = "待付款" }; mockRepo.Setup(r => r.GetById(1)).Returns(order); var service = new OrderService(mockRepo.Object); // 验证 Save 被调用了一次,且参数是状态为"已取消"的订单 mockRepo.Verify(r => r.Save(It.Is<Order>(o => o.Status == "已取消")), Times.Once);
常见报错与修复
| | |
|---|
| | 改为 Mock 接口;或确保类非 sealed、方法为 virtual |
| 异步方法用了 Returns 而非 ReturnsAsync | Invalid setup | 异步用 ReturnsAsync,同步用 Returns |
| NotSupportedException | 验证底层的 Log 方法,而非 LogError/LogInformation |
什么时候用,什么时候不用
适合用 Moq 的场景:
- Service 层的单元测试,用 Moq 替代 Repository
- 调用外部 API(支付、短信、邮件)的类,避免每次测试都真的发请求
不适合用 Moq 的场景:
- 集成测试:想验证 SQL 对不对、数据库连接串通不通,Moq 帮不了你
- 模拟静态方法、
DateTime.Now、File.ReadAllText——Moq 不支持,需要额外封装
避坑指南
坑一:Mock 了 DateTime.Now 这种系统依赖
Moq 不支持直接 Mock 静态调用。在过去,解决方式是手写一个 ITimeProvider 接口做封装。.NET 8 开始官方内置了 TimeProvider 抽象类,你可以直接在业务代码里注入它,测试时用 Moq 模拟或使用 FakeTimeProvider 来控制时间。
坑二:Verify 加得太细
每次改动日志、缓存这种辅助逻辑,测试就跟着改,测试成了重构的阻力。只 Verify 对业务结果有直接影响的方法调用。
坑三:一个测试类里七八个 Mock
如果一个类要注入五六个依赖,测试里就要 Setup 五六个 Mock。这通常说明这个类职责过重,先考虑拆分,再写测试。
用哪个 Mock 框架
Moq 曾经是大多数项目的默认选择。不过 2023 年 8 月,Moq 在 4.20.0 版本中悄悄引入了名为 Anonymously 的依赖项,被社区发现存在隐私收集行为,引发了信任危机(俗称"Moq 供应链事件")。虽然官方随后认错并移除了该依赖,但不少团队因此永久转向了 NSubstitute。
综合来看:对于新项目,NSubstitute 语法更现代、没有历史包袱,值得优先考虑。如果团队已有成熟的 Moq 代码库,继续用 Moq 也没问题——毕竟 Moq 本身依然稳定可用。别在一个代码库里混用两个框架。
一句话总结
Moq 帮你做到三件事:返回假数据让测试跑得通,抛出异常验证容错逻辑,验证方法调用确认行为正确。
它解决的根本问题是:在不改一行业务代码的前提下,让你完全控制外部依赖的行为,从而专心测自己的逻辑。
从下个 PR 开始,把 Mock 框架用起来。不用手写假对象,不用连数据库,单元测试跑在内存里就行。