为什么你的测试在撒谎,以及如何修复?
\n\n想象一下:你编写了完美的C#代码,并用测试覆盖了它,绿色勾号亮起,CI流水线很开心。你发布了版本……然后生产环境崩溃了。熟悉吗?很可能问题不在代码,而在测试。它们测试的不是你真正的代码,而是它的“玩具”版本,这个版本依赖于数据库、文件系统或外部API。
\n\n欢迎来到mock对象和测试夹具的世界——这是支撑C#高质量单元测试的两大支柱。没有它们,你的测试就是脆弱的集成检查,会因环境的微小变化而崩溃。有了它们,你将获得快速、隔离且可靠的测试,真正保护你的代码。
\n\n在本文中,我们将探讨:mock与stub和fake的区别,如何正确设置测试夹具,并展示使用流行库Moq、NSubstitute和AutoFixture的实际示例。开始吧!
\n\nMock、Stub、Fake——有什么区别?(剧透:这很重要)
\n\n在编写代码之前,让我们先统一术语。许多开发者随意使用“mock”这个词,但这会导致混淆。根据Gerard Meszaros的分类(《xUnit Test Patterns》一书),有明确的区分:
\n\n- \n
- Dummy——被传递但从未使用的对象。仅用于填充参数。 \n
- Stub——提供预先准备好的调用响应。你验证的是状态(state verification)。 \n
- Fake——可工作但简化的实现(例如,用InMemoryRepository代替真实数据库)。 \n
- Mock——验证行为的对象(behavior verification)。你断言某个方法被以特定参数调用了。 \n
在实践中,在C#中我们最常使用mock对象(通过Moq或NSubstitute)来验证交互,以及测试夹具(通过AutoFixture)来快速生成测试数据。但让我们按顺序来。
\n\nC#中的Mock对象:隔离依赖
\n\n想象一下,你有一个OrderService服务,它在处理订单时发送电子邮件并将数据保存到数据库。要“真正地”测试它——意味着每次都要发送邮件并写入数据库。这很慢、不可靠且昂贵。
解决方案是依赖隔离。我们将真实对象(EmailSender、DbContext)替换为mock对象,并验证服务是否以正确的参数调用了所需的方法。
\n\n使用Moq的示例(最流行的库)
\n\npublic interface IEmailSender\n{\n void Send(string to, string subject, string body);\n}\n\npublic interface IOrderRepository\n{\n void Save(Order order);\n}\n\npublic class OrderService\n{\n private readonly IEmailSender _emailSender;\n private readonly IOrderRepository _repository;\n\n public OrderService(IEmailSender emailSender, IOrderRepository repository)\n {\n _emailSender = emailSender;\n _repository = repository;\n }\n\n public void PlaceOrder(Order order)\n {\n // 业务逻辑\n _repository.Save(order);\n _emailSender.Send(order.CustomerEmail, "订单已确认", $"您的订单 #{order.Id} 已处理。");\n }\n}\n\n现在让我们使用Moq编写一个测试:
\n\n[TestFixture]\npublic class OrderServiceTests\n{\n [Test]\n public void PlaceOrder_ShouldSendEmail_WhenOrderIsValid()\n {\n // Arrange\n var mockEmailSender = new Mock<IEmailSender>();\n var mockRepository = new Mock<IOrderRepository>();\n var service = new OrderService(mockEmailSender.Object, mockRepository.Object);\n var order = new Order { Id =