×

.NET 单元测试利器:Moq 核心用法与避坑指南

独孤求败 独孤求败 发表于2026-06-30 10:06:30 浏览27 评论0

抢沙发发表评论

你是不是也有这种情况:想给 Service 层写个单元测试,结果发现要连数据库、要调第三方 API、要读配置文件……折腾半天,测试没写出来,环境先崩了。

这不是你的问题,是被测代码依赖太多外部资源的问题。单元测试要的是隔离——只测当前类的逻辑,不碰任何真实的外部依赖。

Moq 就干一件事: 给被测类造一个"替身",把外部依赖全部替换掉,让你想让它返回什么就返回什么,想让它抛异常它就抛异常。

Moq 是什么

Moq(发音同"mock-you")是 .NET 平台上最常用的 Mock 框架,一个开源库。它能在运行时动态生成一个"假对象"来替代真实的依赖,让你在测试中完全控制这个假对象的行为。

为什么不用手写假对象?

假设你要测一个 UserService,它依赖 IUserRepository。不用 Moq 的话,你得手动写一个类实现 IUserRepository 接口,每个方法都要写一遍"返回什么"。测试多了,项目里全是这种"空壳类",重复劳动,维护成本还高。

Moq 帮你自动生成这些"空壳类",几行代码就能搞定。

官方资源:
GitHub:


https://github.com/moq/moq

图片

怎么引入

测试项目中安装对应的 NuGet 包即可:

dotnet add package Moq
图片

Moq 不需要在 Program.cs 中做任何配置——它只在测试项目中使用,不影响运行时代码。

快速上手

示例一:用假数据替代真实数据库查询

这是 Moq 最基础的用途:让依赖返回你指定的数据,避免真的去查数据库。

// 实体类定义
 
public class User
{
    public int Id { getset; }
    public string Name { getset; }
}
 
 
public interface IUserRepository
{
    User GetById(int id);
}
 
 
public class UserService
{
    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;
    }
}

测试代码:

using Moq;
 
using Xunit;
 
[Fact]
 
public void GetUserDisplayName_用户存在_返回用户名()
{
    // 1. 创建替身
    var mockRepo = new Mock<IUserRepository>();
 
    // 2. 安排替身行为:当 GetById(1) 被调用时,返回一个 User 对象
    mockRepo.Setup(r => r.GetById(1))
            .Returns(new User { Id = 1, Name = "张三" });
 
    // 3. 把替身注入被测类
    var service = new UserService(mockRepo.Object);
 
    // 4. 执行被测方法
    var result = service.GetUserDisplayName(1);
 
    // 5. 验证结果——这里返回的是"张三",而不是真的去数据库查出来的
    Assert.Equal("张三", result);
}

关键点: 被测代码一行没改,_repo.GetById(id) 在测试环境里被 Moq 接管了,返回你指定的值。上线后它该查数据库还是查数据库。

示例二:模拟数据库超时,验证代码会不会崩

这个场景在真实项目里很难复现——你总不能真把数据库弄挂。Moq 可以轻松模拟。

public interface IUserRepository
{
    Task<User> GetUserAsync(int id);
}
 
 
// 实体类定义
 
public class User
{
    public int Id { getset; }
    public string Name { getset; }
}
 
 
public class UserProfileService
{
    private readonly IUserRepository _repo;
    private readonly ILogger<UserProfileService> _logger;
 
    public UserProfileService(IUserRepository repo, ILogger<UserProfileService> logger)
    {
        _repo = repo;
        _logger = logger;
    }
 
    public async Task<string> GetDisplayNameAsync(int id)
    {
        try
        {
            var user = await _repo.GetUserAsync(id);
            return user == null ? "未找到" : user.Name;
        }
        catch (TimeoutException ex)
        {
            _logger.LogError(ex, "数据库查询超时");
            return "系统繁忙,请稍后重试";
        }
    }
}

测试"数据库挂了"的场景:

[Fact]
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 去验证
    // 正确做法是验证底层 Log 方法被调用
    mockLogger.Verify(
        x => x.Log(
            LogLevel.Error,
            It.IsAny<EventId>(),
            It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("数据库查询超时")),
            It.IsAny<TimeoutException>(),
            It.IsAny<Func<It.IsAnyType, Exception, string>>()),
        Times.Once);
}

这里有个新手极易踩的坑:LogError 是 ILogger 的扩展方法,不是接口方法。直接用 Verify(l => l.LogError(...)) 会报 NotSupportedException。必须验证底层的 Log 方法。

示例三:验证方法有没有被正确调用

有些方法没有返回值,只负责"保存"或"发消息"。这时候需要 Moq 来验证行为。

// 实体类定义
 
public class Order
{
    public int Id { getset; }
    public string Status { getset; }
}
 
 
public interface IOrderRepository
{
    void Save(Order order);
    Order GetById(int id);
}
 
 
public 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 == "待付款")
        {
            order.Status = "已取消";
            _repo.Save(order);
        }
    }
}
 
[Fact]
 
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);
 
    service.CancelOrder(1);
 
    // 验证 Save 被调用了一次,且参数是状态为"已取消"的订单
    mockRepo.Verify(r => r.Save(It.Is<Order>(o => o.Status == "已取消")), Times.Once);
}

常见报错与修复

报错场景
报错信息(示意)
怎么修
尝试 Mock 密封类或非虚方法
编译错误
改为 Mock 接口;或确保类非 sealed、方法为 virtual
异步方法用了 Returns 而非 ReturnsAsync
Invalid setup
异步用 ReturnsAsync,同步用 Returns
对 ILogger 扩展方法用 Verify
NotSupportedException
验证底层的 Log 方法,而非 LogError/LogInformation

什么时候用,什么时候不用

适合用 Moq 的场景:

  • Service 层的单元测试,用 Moq 替代 Repository
  • 调用外部 API(支付、短信、邮件)的类,避免每次测试都真的发请求
  • 消息队列的生产者/消费者,验证消息是否被正确发送
  • 需要模拟异常(超时、网络错误)来验证容错逻辑

不适合用 Moq 的场景:

  • 集成测试:想验证 SQL 对不对、数据库连接串通不通,Moq 帮不了你
  • 端到端测试:全链路验证,不需要 Mock
  • 模拟静态方法DateTime.NowFile.ReadAllText——Moq 不支持,需要额外封装

避坑指南

坑一:Mock 了 DateTime.Now 这种系统依赖

Moq 不支持直接 Mock 静态调用。在过去,解决方式是手写一个 ITimeProvider 接口做封装。.NET 8 开始官方内置了 TimeProvider 抽象类,你可以直接在业务代码里注入它,测试时用 Moq 模拟或使用 FakeTimeProvider 来控制时间。

坑二:Verify 加得太细

每次改动日志、缓存这种辅助逻辑,测试就跟着改,测试成了重构的阻力。只 Verify 对业务结果有直接影响的方法调用。

坑三:一个测试类里七八个 Mock

如果一个类要注入五六个依赖,测试里就要 Setup 五六个 Mock。这通常说明这个类职责过重,先考虑拆分,再写测试。

用哪个 Mock 框架

特点
适合谁
Moq
老牌王者,资料最多
有历史包袱的老项目
NSubstitute
语法更简洁,无历史包袱
新项目、新手团队,学习成本低
FakeItEasy
风格类似 Moq
个人偏好

Moq 曾经是大多数项目的默认选择。不过 2023 年 8 月,Moq 在 4.20.0 版本中悄悄引入了名为 Anonymously 的依赖项,被社区发现存在隐私收集行为,引发了信任危机(俗称"Moq 供应链事件")。虽然官方随后认错并移除了该依赖,但不少团队因此永久转向了 NSubstitute。

综合来看:对于新项目,NSubstitute 语法更现代、没有历史包袱,值得优先考虑。如果团队已有成熟的 Moq 代码库,继续用 Moq 也没问题——毕竟 Moq 本身依然稳定可用。别在一个代码库里混用两个框架。

一句话总结

Moq 帮你做到三件事:返回假数据让测试跑得通,抛出异常验证容错逻辑,验证方法调用确认行为正确

它解决的根本问题是:在不改一行业务代码的前提下,让你完全控制外部依赖的行为,从而专心测自己的逻辑

从下个 PR 开始,把 Mock 框架用起来。不用手写假对象,不用连数据库,单元测试跑在内存里就行。


群贤毕至

访客