为什么你的Java测试会失败(以及如何修复)
\n\n你编写了完美的代码。服务运行正常,逻辑无懈可击,bug只存在于别人的项目中。但当你运行测试时……这里出现数据库连接错误,那里外部API没有响应,第三个测试甚至抛出NullPointerException,因为配置没有正确加载。熟悉吗?
\n\n问题在于,传统测试依赖于外部环境。数据库、REST服务、文件系统——所有这些都使测试变得脆弱、缓慢且不可预测。解决方案是Mock对象和测试夹具。这不仅仅是时髦的词汇,而是能将混乱变为有序的工具。
\n\n在本文中,我们将通过实际示例,探讨如何在Java测试中正确使用mock和测试夹具。你将了解stub与mock的区别,如何为复杂场景配置测试夹具,以及为什么Mockito是你最好的朋友。开始吧!
\n\nMock对象:无痛模拟
\n\n什么是mock,为什么需要它?
\n\nMock对象是对真实组件的模拟,其行为完全由你控制。无需启动数据库或调用外部API,你只需创建一个“假对象”,它返回预先定义好的数据。
\n\n想象一下:你正在测试一个发送电子邮件通知的服务。要进行真实测试,你需要启动邮件服务器、配置SMTP、检查队列……而使用mock对象,你只需验证服务是否使用正确的参数调用了发送方法。快速、干净、可控。
\n\nMock vs Stub:有什么区别?
\n\n新手经常混淆这两个概念。让我们来理清它们:
\n\n- \n
- Stub — 返回固定数据。你告诉它:“当调用getUser()方法时,返回一个名为'John'的User对象”。Stub不检查它被调用了多少次。 \n
- Mock — 不仅返回数据,还验证行为。你可以说:“确保sendEmail()方法被恰好调用一次,并且使用了特定的地址”。 \n
简单来说:stub是“给我数据”,mock是“给我数据并检查我是如何被使用的”。在实践中,我们更常使用mock对象,因为它们提供了更多的控制。
\n\n使用Mockito编写第一个mock
\n\nMockito是Java中最流行的mock库。它与JUnit 5和Spring Boot集成,语法非常简单,你五分钟就能记住。
\n\n假设你有一个OrderService服务,它依赖于OrderRepository仓库。我们将创建一个mock来代替真实的仓库:
import static org.mockito.Mockito.*;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.junit.jupiter.api.extension.ExtendWith;\n\n@ExtendWith(MockitoExtension.class)\npublic class OrderServiceTest {\n\n @Mock\n private OrderRepository orderRepository;\n\n @Test\n void testCreateOrder() {\n // 创建mock对象\n OrderService orderService = new OrderService(orderRepository);\n \n // 配置mock行为\n when(orderRepository.save(any(Order.class)))\n .thenReturn(new Order(1L, "test@example.com", 100.0));\n \n // 调用被测试的方法\n Order result = orderService.createOrder("test@example.com", 100.0);\n \n // 验证结果\n assertThat(result.getId()).isEqualTo(1L);\n // 验证save方法被恰好调用一次\n verify(orderRepository, times(1)).save(any(Order.class));\n }\n}\n\n在这个例子中,我们:
\n- \n
- 使用
@Mock注解为OrderRepository创建了一个mock。 \n - 配置了当调用
save()方法时,应返回一个预先创建的订单。 \n - 验证了