赞
踩
本人以前在Java项目开发中有一大痛点就是写单元测试,因为部署上线时,在 CI/CD 流水线中在对代码行覆盖率有强卡点,代码行覆盖率必须达到90%才能继续推进部署。回想一下以前排斥写单元测试的主要原因有如下几点:
1、心理上排斥写单元测试,觉得很繁琐,为了代码行覆盖率去写测试
2、写单元测试没有比较好的实践经验,遇到不好覆盖的代码,不知道如何处理,也不知道如何重构代码
如何克服这两个问题?第一个是心理问题,需要真正认识到写单元测试的好处,这样才能够接受写单元测试是开发过程中的必要步骤。第二个是方法问题,需要学习写单元测试的方法和最佳实践,这样在写单元测试时才能知道如何下手。
本文主要从理论和实践两个方面介绍一下如何在 Java 项目中写单元测试,在理论篇中重点说明写单元测试的好处与规范,在实践篇中介绍一下写单元测试的一些实践方法,重点介绍在单元测试和集成测试中如何使用 Mock 对象。
通过本文的学习,你将了解到写单元测试的方法论和最佳实践,也许能克服前面提到的两个问题,也就不会排斥写单元测试了,并且能够写出更高效、更可靠的单元测试和集成测试,进而提高整体的软件质量。
在Java项目中,单元测试和集成测试是软件测试方法中的两个重要组成部分,它们在测试的范围、目的和实现方法上有所区别:
总的来说,单元测试和集成测试在测试策略中扮演着互补的角色。单元测试通过快速、频繁地验证小块功能来保证代码质量,而集成测试通过在更复杂的环境中验证组件协同工作的情形来确保整个系统的稳定性和可靠性。在现代软件开发实践中,单元测试和集成测试常常被结合起来使用,以实现更全面的测试覆盖。
单元测试和集成测试是软件开发过程中保证整个系统稳定性和可靠性的关键实践,它们带来了许多好处:
总体而言,单元测试和集成测试提供了一个强大的安全网,可以在整个软件开发生命周期中确保软件质量。它们有助于开发团队及时发现和解决问题,提高开发效率,减少后期维护的负担,并最终提供更稳定、更可靠的软件产品。
编写单元测试和集成测试时遵循一些最佳实践可以提高测试的可维护性、有效性和效率。以下是一些最佳实践和建议:
以下是写出容易被单元测试代码的建议:
总之,编写容易被单元测试的代码需要注重代码的可测试性、可读性和可扩展性,遵循良好的设计原则和编码规范,使用合适的工具和技术。遵循这些最佳实践和建议可以帮助你编写出更高效、更可靠的单元测试和集成测试,进而提高整体的软件质量。
在Mockito的早期版本中,模拟静态方法是不支持的,但从Mockito 3.4.0开始,通过使用mockito-inline模块,可以对静态方法进行模拟。假设有一个静态方法需要被模拟,下面是如何进行操作的示例:
首先,确保你在项目中添加了正确的Mockito依赖。如果你使用Maven,你需要添加mockito-core和mockito-inline依赖:
- <dependencies>
- <!-- 其他依赖 -->
-
- <!-- Mockito的核心库 -->
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <version>3.4.0</version> <!-- 或者更高版本 -->
- <scope>test</scope>
- </dependency>
-
- <!-- 支持模拟静态方法的库 -->
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-inline</artifactId>
- <version>3.4.0</version> <!-- 或者更高版本 -->
- <scope>test</scope>
- </dependency>
- </dependencies>
然后,使用Mockito的try资源块来创建一个模拟的静态方法调用。下面是一个如何对静态方法进行模拟的例子:
- import org.mockito.MockedStatic;
- import org.mockito.Mockito;
- import static org.junit.Assert.assertEquals;
- import static org.mockito.Mockito.*;
-
- class SomeClass {
- public static String staticMethod() {
- return "实际的静态方法调用";
- }
- }
-
- public class SomeClassTest {
-
- @Test
- public void testStaticMethodMocking() {
- // 开始模拟静态方法
- try (MockedStatic<SomeClass> mockedStatic = Mockito.mockStatic(SomeClass.class)) {
- // 指定静态方法的期望行为
- mockedStatic.when(SomeClass::staticMethod).thenReturn("模拟的静态方法调用");
-
- // 调用静态方法并验证模拟行为是否生效
- String result = SomeClass.staticMethod();
- assertEquals("模拟的静态方法调用", result);
-
- // 验证静态方法是否被调用
- mockedStatic.verify(SomeClass::staticMethod);
- }
- // 在资源块结束后,静态方法的模拟将会自动失效
- }
- }
在这个例子中,我们使用Mockito的mockStatic方法来模拟SomeClass类的静态方法staticMethod。在try资源块中,我们设置了期望行为,然后调用了静态方法并验证了结果。当try资源块结束后,静态方法的模拟自动失效,恢复原始行为。
请注意,模拟静态方法时,应该只在必要时使用,因为这可能会隐藏代码中的设计问题。尽量通过重构来避免对静态方法的依赖,使代码更容易测试。
在Mockito中,对抽象方法进行单元测试通常涉及创建一个抽象类的具体子类或模拟实例。以下是如何使用Mockito对抽象方法进行单元测试的基本步骤:
如果你的抽象类只有少数几个方法需要被模拟,你可以创建一个匿名子类,并在其中实现这些方法:
- @Test
- public void testAbstractMethod() {
- // 创建抽象类的匿名实现
- AbstractClass testInstance = new AbstractClass() {
- @Override
- public String abstractMethod() {
- return "mocked response";
- }
- };
-
- // 使用testInstance进行测试
- assertEquals("mocked response", testInstance.abstractMethod());
- }
在这个简单的例子中,我们重写了abstractMethod并返回了一个已经模拟的响应字符串。
Mockito允许你直接模拟抽象类的具体实例,并为其抽象方法指定行为,你可以使用mock()方法创建一个模拟并使用when()来指定期望的行为。
- import static org.mockito.Mockito.mock;
- import static org.mockito.Mockito.when;
- import static org.junit.Assert.assertEquals;
- import org.junit.Test;
-
- public abstract class AbstractClass {
- public abstract String abstractMethod();
- }
-
- public class AbstractClassTest {
-
- @Test
- public void testAbstractMethodWithMockito() {
- // 使用Mockito创建AbstractClass的模拟实例
- AbstractClass mockAbstractClass = mock(AbstractClass.class);
-
- // 配置模拟行为:当调用abstractMethod时返回"mocked response"
- when(mockAbstractClass.abstractMethod()).thenReturn("mocked response");
-
- // 测试模拟的方法
- assertEquals("mocked response", mockAbstractClass.abstractMethod());
- }
- }
这种方式不需要实际创建一个子类实例。Mockito允许你模拟抽象方法,并定义方法被调用时的行为。
如果你想要对抽象类的实例进行部分模拟(模拟一些方法,而其他方法则保持原有行为),你可以使用Mockito的spy方法。但是,这通常需要创建抽象类的一个具体子类实例。
- import static org.mockito.Mockito.doReturn;
- import static org.mockito.Mockito.spy;
- import static org.junit.Assert.assertEquals;
- import org.junit.Test;
-
- public abstract class AbstractClass {
- public abstract String abstractMethod();
- public String concreteMethod() {
- return "concrete response";
- }
- }
-
- public class AbstractClassTest {
-
- @Test
- public void testAbstractMethodWithSpy() {
- // 创建AbstractClass的匿名实现,并创建一个spy
- AbstractClass spyAbstractClass = spy(new AbstractClass() {
- @Override
- public String abstractMethod() {
- return "actual implementation";
- }
- });
-
- // 修改spy,使得abstractMethod返回"mocked response"
- doReturn("mocked response").when(spyAbstractClass).abstractMethod();
-
- // 测试模拟的方法
- assertEquals("mocked response", spyAbstractClass.abstractMethod());
- // 测试未被模拟的具体方法
- assertEquals("concrete response", spyAbstractClass.concreteMethod());
- }
- }
在这个例子中,我们创建了AbstractClass的一个匿名实现,并对它进行了部分模拟(spy)。然后我们修改了abstractMethod的行为,而concreteMethod保持原有实现。
以上三种方法中,使用Mockito模拟抽象类是最常见和通用的方法,它不需要额外的类定义,也不需要实际实现抽象方法。但在某些情况下,如果你需要测试抽象类的方法实现,创建一个匿名子类或使用spy方法可能是更合适的选择。
在单元测试中测试异常通常涉及到两个方面:
以下是在Java中测试异常的几种方法:
- @Test
- public void testDivide() {
- try {
- // 构造输入,并调用被测方法
- var output = testFunction(input);
- fail("no exception");
- } catch (Exception e) {
- assertTrue(expectedException);
- assertTrue(e.getMessage().contains("some message in exception"));
- }
- }
- @Test
- public void testDivide() {
- try {
- int i = 1/0;
- fail("Expected an ArithmeticException to be thrown");
- } catch (ArithmeticException ae) {
- assertTrue(true);
- }
- }
由于构造的单测目的就是为了测试抛出异常的正确性,所以没有抛出异常需要认为测试不通过,标识为fail。
JUnit 4 中你可以使用@Test
注解的 expected 属性来指定预期抛出的异常类型。
- import org.junit.Test;
-
- public class ExceptionTest {
-
- @Test(expected = IllegalArgumentException.class)
- public void whenExceptionThrown_thenExpectationSatisfied() {
- MyClass myClass = new MyClass();
- myClass.methodThatShouldThrowException();
- }
- }
使用@Test注解的expected属性来指定期望抛出的异常类型为 IllegalArgumentException
。当测试的方法抛出IllegalArgumentException
异常时,测试将会通过。如果没有抛出异常或抛出了不同类型的异常,测试将会失败。方法2的不足是无法判断异常中e.getMessage()的具体信息内容。
当你使用Mockito框架进行模拟测试时,也可以轻松地测试抛出异常的情况,配置mock对象抛出异常:
- import org.junit.Test;
- import org.mockito.Mockito;
-
- public class ExceptionTest {
-
- @Test(expected = IOException.class)
- public void whenConfiguredMockException_thenThrow() throws IOException {
- MyCollaborator collaborator = Mockito.mock(MyCollaborator.class);
- Mockito.when(collaborator.doSomething()).thenThrow(new IOException());
-
- collaborator.doSomething(); // 这将抛出IOException异常
- }
- }
在这个例子中,MyCollaborator是一个被模拟的协作类,我们配置了它的doSomething方法在调用时抛出IOException。然后我们尝试调用这个方法,并验证了是否抛出了异常。
- public class ExceptionTest {
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
- @Test
- public void testDivide() {
- exception.expect(ArithmeticException.class);
- exception.expectMessage("cannot divide 0");
- int i = 1/0;
- }
- }
声明了一个ExpectedException对象exception,并使用@Rule注解将它声明为测试规则。在测试方法中,利用exception.expect方法指定期望抛出的异常类型,利用exception.expectMessage方法指定期望抛出异常中包含的信息。
JUnit 5提供了更灵活的异常测试机制,assertThrows
是JUnit 5中用于捕获和验证异常的方法。它可以验证异常的类型,并允许对异常对象进行进一步的断言。
- import org.junit.jupiter.api.Test;
- import static org.junit.jupiter.api.Assertions.assertThrows;
-
- public class ExceptionTest {
-
- @Test
- public void whenDerivedExceptionThrown_thenAssertionSucceeds() {
- MyClass myClass = new MyClass();
- Exception exception = assertThrows(IllegalArgumentException.class, () -> {
- myClass.methodThatShouldThrowException();
- });
-
- // 可选的额外断言,比如检查异常消息
- assertEquals("Expected message", exception.getMessage());
- }
- }
assertThrows方法会返回捕获到的异常,这样你就可以对异常对象进行更详细的断言。
通过这些方法,你可以确保你的单元测试可以有效地验证异常情况,无论是确保方法在给定的条件下抛出正确的异常,还是验证异常处理逻辑的正确性,良好的异常测试覆盖可以显著提高代码的健壮性和质量。
在软件测试中为什么需要 mock 对象?mock 对象是用来模拟真实对象行为的假对象,mock 对象可以帮助我们在单元测试和集成测试中隔离测试的组件,确保测试的准确性和独立性。在不同类型的测试中使用 mock 对象的原理相似,但具体应用可能会有所不同。
以下是使用 mock 对象的一些具体原因:
使用 mock 对象是一个在软件开发过程中广泛采用的最佳实践,尤其是当采用测试驱动开发(TDD)或行为驱动开发(BDD)方法时。然而,mock 对象应该谨慎使用,因为它们可能隐藏真实环境中的问题,因此在完成单元测试和集成测试后,仍然需要在真实环境中进行系统测试和验收测试。
接下来我将介绍一下在单元测试和集成测试中如何使用 mock 对象。
单元测试通常聚焦于测试系统中的一个单一组件,如一个类或者方法。在这个层面上,你可能会使用 mock 对象来模拟该组件依赖的其他组件的行为。这样做可以确保你的测试仅关注于当前组件的行为,并且不会受到外部依赖的影响。在单元测试中使用 mock 对象的步骤如下:
Mockito是一个流行的Java单元测试框架,它允许你创建和配置模拟对象,用于隔离需要测试的代码。以下是一些基本的Mockito使用方法。
首先,确保添加Mockito依赖到你的项目中。如果你使用Maven,可以在pom.xml中添加如下依赖:
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <version>3.11.2</version>
- <scope>test</scope>
- </dependency>
以下是一些基本的Mockito使用方法:
1. 创建模拟对象
在使用Mockito创建模拟对象时,有几种常用的方法:
1、使用mock()方法直接创建
- // 创建一个模拟的List对象
- List mockedList = mock(List.class);
2、使用注解@Mock
在测试类中,你可以声明一个带有@Mock
注解的字段。为了初始化这些注解,你需要在测试初始化时调用MockitoAnnotations.initMocks(this)
,或者使用MockitoJUnitRunner
运行测试类。
- import org.mockito.Mock;
- import org.mockito.MockitoAnnotations;
- import org.junit.Before;
- import org.junit.Test;
-
- public class ExampleTest {
-
- @Mock
- private List mockedList;
-
- @Before
- public void initMocks() {
- MockitoAnnotations.initMocks(this);
- }
-
- @Test
- public void testMethod() {
- // 使用模拟的mockedList对象进行测试
- }
- }
如果你使用 JUnit 4,可以用@RunWith(MockitoJUnitRunner.class)
代替MockitoAnnotations.initMocks(this)
。
- import static org.mockito.Mockito.*;
- import org.mockito.Mock;
- import org.mockito.junit.MockitoJUnitRunner;
- import org.junit.runner.RunWith;
- import org.junit.Test;
-
- @RunWith(MockitoJUnitRunner.class)
- public class ExampleTest {
-
- @Mock
- private MyCollaborator collaborator;
-
- @Test
- public void testMethod() {
- // 配置模拟对象
- when(collaborator.someMethod()).thenReturn("expected response");
-
- // 调用被测试的方法
- MyClass myClass = new MyClass(collaborator);
- myClass.performAction();
-
- // 验证方法是否被调用
- verify(collaborator).someMethod();
- }
- }
在这个例子中,我们不需要显式调用MockitoAnnotations.initMocks(this)
,因为MockitoJUnitRunner
会帮我们完成模拟对象的初始化。@RunWith(MockitoJUnitRunner.class)
注解告诉JUnit使用Mockito提供的测试运行器MockitoJUnitRunner
来运行测试。这个运行器提供了一些有用的功能,可以简化Mockito在测试中的使用,并确保更好的测试实践。
以下是使用MockitoJUnitRunner
的一些好处:
MockitoAnnotations.initMocks(this)
方法来初始化这些模拟对象。MockitoJUnitRunner
会负责在每个测试方法执行前自动初始化所有@Mock
注解的字段。MockitoJUnitRunner
可以减少编写初始化代码的需要,让测试代码更加简洁。when()
方法时,它可能会抛出有用的错误信息。如果你正在使用JUnit 5,那么你不需要 @RunWith
注解,因为 JUnit 5 有一个内建的扩展模型。在JUnit 5中,你可以使用@ExtendWith(MockitoExtension.class)
注解来达到类似的效果。
- import org.mockito.Mock;
- import org.mockito.junit.jupiter.MockitoExtension;
- import org.junit.jupiter.api.extension.ExtendWith;
- import org.junit.jupiter.api.Test;
-
- @ExtendWith(MockitoExtension.class)
- public class ExampleTest {
-
- @Mock
- private List mockedList;
-
- @Test
- public void testMethod() {
- // 使用模拟的mockedList对象进行测试
- }
- }
2. 指定模拟对象的行为
- // 配置模拟对象返回特定的值
- when(mockObject.myMethod("some input")).thenReturn("expected output");
3. 验证测试结果
验证方法调用:
- // 验证myMethod是否被调用了一次
- verify(mockObject).myMethod("some input");
-
- // 验证myMethod是否从未被调用
- verify(mockObject, never()).myMethod("some input");
-
- // 验证myMethod是否至少被调用了一次
- verify(mockObject, atLeastOnce()).myMethod("some input");
-
- // 验证myMethod是否被调用了指定次数
- verify(mockObject, times(2)).myMethod("some input");
验证返回值:
- // then
- Assert.assertNotNull(result);
- Assert.assertEquals(result.getResponseCode(), 200);
-
- // 其他常用的断言函数
- Assert.assertTrue(...);
- Assert.assertFalse(...);
- Assert.assertSame(...);
- Assert.assertEquals(...);
- Assert.assertArrayEquals(...);
4. 模拟抛出异常
- // 配置模拟对象在调用myMethod时抛出异常
- when(mockObject.myMethod("some input")).thenThrow(new RuntimeException());
5. 参数匹配
Mockito提供了参数匹配器,允许灵活地指定输入参数,这些匹配器可以是any(), eq(), anyInt()等。
- // 使用anyString()匹配器来匹配任何String类型的输入
- when(mockObject.myMethod(anyString())).thenReturn("expected output");
-
- // 使用eq()匹配器来匹配特定的值
- when(mockObject.myMethod(eq("specific input"))).thenReturn("expected output");
6. 模拟void方法
对于没有返回值的方法(void方法),你可以使用doNothing()、doThrow()、doAnswer()来进行模拟。
- // 配置void方法什么都不做
- doNothing().when(mockObject).myVoidMethod("some input");
-
- // 配置void方法抛出异常
- doThrow(new RuntimeException()).when(mockObject).myVoidMethod("some input");
7. 连续调用
thenReturn()方法和thenThrow()方法可以链式调用,以设置连续调用的行为。
// 第一次调用返回值"first call",第二次调用返回值"second call" when(mockObject.myMethod("input")).thenReturn("first call").thenReturn("second call");
8. 模拟真实调用(部分模拟)
有时,你可能想调用真实的方法实现,而不是返回模拟的结果。这可以通过spy()来实现。使用spy时,除非你显式指定了模拟的行为,否则调用对象的方法都会执行真实的逻辑。
- // 创建一个“间谍”对象
- MyClass spyObject = spy(new MyClass());
-
- // 配置间谍对象的特定方法调用真实方法
- doCallRealMethod().when(spyObject).myMethod("some input");
集成测试通常涉及到多个组件的相互作用,目的是验证它们能够协同工作。在这个层面上,mock 对象可以用来模拟外部系统或服务,例如数据库、网络服务或消息队列。这样做可以提供一个可控的环境来测试组件的集成。在集成测试中使用 mock 对象的步骤如下:
在使用 mock 对象时,重要的是要理解它们并不是替代完整的集成测试或系统测试,而是作为测试策略中的一部分。Mock 对象能够帮助我们在不受外部环境影响的情况下测试代码,但它们不能完全模拟真实世界的复杂性。因此,在测试周期的后期,还需要执行含有真实依赖的测试,以确保系统在真实环境下的表现。
Spring Boot 包含一个 @MockBean
注解,可用于在 ApplicationContext
中为 bean 定义 Mockito 模拟,可以使用注解来添加新 bean 或替换单个现有 bean ,@MockBean
可以直接用于测试类、测试中的字段或 @Configuration 类和字段。
在Spring Boot的测试中,当你使用@SpringBootTest
注解时,它会加载完整的应用程序上下文并自动启用 Mock 的功能,如果在你的测试类中没有使用 @SpringBootTest
,则必须手动添加 @TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
示例如下:
- import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
- import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
- import org.springframework.test.context.ContextConfiguration;
- import org.springframework.test.context.TestExecutionListeners;
-
- @ContextConfiguration(classes = MyConfig.class)
- @TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
- class MyTests {
- // ...
- }
@MockBean
不能用于模拟在应用程序上下文刷新期间执行的 bean 的行为,因为在执行测试时应用程序上下文刷新已经完成,现在配置模拟行为为时已晚。在这种情况下,我们建议使用 @Bean 方法来创建和配置模拟。
你可以使用@MockBean
注解来添加一个模拟到Spring应用程序上下文中。这个Mock会替换任何现有的同类型的Bean,因此当你的服务尝试使用该Bean时,它会使用你的Mock版本,而不是实际的实例。
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.boot.test.mock.mockito.MockBean;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.test.context.junit4.SpringRunner;
- import org.junit.runner.RunWith;
- import org.junit.Test;
- import org.mockito.Mockito;
-
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class ExternalServiceTest {
-
- @MockBean
- private ExternalService externalService; // 需要模拟的外部服务
-
- @Autowired
- private ServiceUnderTest serviceUnderTest; // 测试的目标服务
-
- @Test
- public void testServiceMethod() {
- // 设置模拟行为
- Mockito.when(externalService.callExternalService()).thenReturn("Mock Response");
-
- // 调用服务方法,它会使用模拟的外部服务
- serviceUnderTest.serviceMethod();
-
- // 验证外部服务是否被调用
- Mockito.verify(externalService).callExternalService();
- }
- }
在这个例子中,ExternalService是我们想要模拟的外部服务,而ServiceUnderTest是我们正在测试的服务。
然而,由于@MockBean
是基于对 Bean 的完整声明周期进行 Mock,为了保证不同测试用例之间被 Mock 的 Bean 不会互相干扰,使用了不同 @MockBean
注解的测试用例之间不再复用 Spring 上下文,从而导致整个集成测试执行期间会启动多次 Spring 上下文,这会带来一些负面问题:
如何解决这个问题,可以参考 InjectorMockTestExecutionListener.java。它的原理就是:
在测试类开始执行前,先解析相关注解确定需要生成哪些 Mock/Spy 以及对应的注入目标(可能是 Bean 或者 SOFA 服务)。
在测试方法执行前,会将目标中的相应字段替换成 Mock/Spy,并执行测试方法。
在测试类执行完毕后,会将注入的 Mock/Spy 重置回原来的值,保证 Spring 上下文不被污染,因此 Spring Test 可以直接复用缓存的上下文。
在Mockito框架中,mock和spy是用于创建测试的两种不同的方法,它们在单元测试中有着不同的应用场景。
使用mock方法创建的是一个完全的模拟对象,这种模拟对象没有任何与原始类相关的行为,每个非void方法的默认行为都是返回相应类型的默认值(比如0、false、null等),而void方法则不执行任何操作。你需要为这个模拟对象手动设置所有希望在测试中调用的方法的期望行为。
使用mock的场景是你想完全控制一个类的行为,通常是因为这个类很复杂,或者它的行为依赖于外部系统,如数据库或网络服务。
- List mockedList = mock(List.class);
- when(mockedList.size()).thenReturn(100);
在上面的代码中,mockedList对象是一个完全的模拟对象,其size()方法的行为被指定为返回100。
使用spy方法创建的是一个部分模拟的对象,这种对象的默认行为是调用实际的方法,但你可以覆盖某些方法的行为来满足测试需求。它基于一个已经存在的实例,可以让你在保持大部分原有行为的基础上,只修改其中一部分方法。
Spy通常用于那些不方便或不需要完全模拟的场景。比如,当你想测试一个类的某个功能,而这个功能依赖于类中其他已经被良好测试和验证的方法时。
- List list = new ArrayList();
- List spyList = spy(list);
- doReturn(100).when(spyList).size(); // 正确的使用方式
- // when(spyList.size()).thenReturn(100); // 错误的使用方式,size()会被调用
在上面的代码中,spyList是基于list的一个spy对象,它的大部分行为都和list一样,但是size()方法的行为被修改为返回100。
doReturn()/doThrow()/doAnswer()
等语法,而不是when()/thenReturn()/thenThrow()/thenAnswer()
等语法,因为后者会首先调用一次真实方法,然后再设置存根。- doReturn(100).when(spyList).size(); // 正确的使用方式
- // when(spyList.size()).thenReturn(100); // 错误的使用方式,size()会被调用
综上所述,mock主要用于完全模拟对象,而spy用于在需要时只覆盖部分方法的部分模拟对象。在单元测试中,通常推荐使用mock,因为这可以保持测试的独立性和可预测性。Spy则在需要对现有实例进行微调时使用。
如果你在使用@MockBean进行单元测试,但是发现mock的bean为null,这通常意味着Spring的测试上下文没有正确设置或者@MockBean没有被正确应用。下面是一些可能导致这种情况的原因以及如何解决它们:
1. 确保包含Spring Boot测试依赖
首先,确认你的项目中已经包含了Spring Boot测试相关的依赖。
对于Maven,应该包括以下依赖:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
对于Gradle,应该添加以下依赖:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2. 使用正确的测试注解
确保你使用了正确的测试注解,如@SpringBootTest、@DataJpaTest、@WebMvcTest等,这取决于你的测试类型。
例如:
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class YourTest {
- // ...
- }
对于JUnit 5,使用以下注解:
- @ExtendWith(SpringExtension.class)
- @SpringBootTest
- public class YourTest {
- // ...
- }
3. 在测试类中使用@MockBean
确保@MockBean注解是被添加在测试类中,而不是在测试方法或其他地方。
- @SpringBootTest
- public class YourTest {
-
- @MockBean
- private YourService yourService;
-
- // ...
- }
4. 确保使用了Spring的测试运行器
当使用JUnit 4时,确保你的测试类使用了@RunWith(SpringRunner.class)
或@RunWith(SpringJUnit4ClassRunner.class)
。
5. 正确初始化Mockito
如果你不使用@SpringBootTest
,而是用@ExtendWith(MockitoExtension.class)
来进行普通的单元测试,那么你不能使用@MockBean,而应该使用@Mock和@InjectMocks。
6. 避免循环依赖
如果你的测试中出现了循环依赖,它可能会导致@MockBean
无法正确工作。检查你的应用配置和Bean之间的依赖关系,确保没有循环依赖。
7. 清理缓存的测试上下文
有时候,缓存的测试上下文可能会产生问题。尝试在IDE中清除构建并重新运行测试,或者在命令行中使用Maven或Gradle的清理命令。
8. 检查测试配置文件
如果你的项目中有多个测试配置文件,确认没有其他配置覆盖了你的MockBean。
如果以上步骤都无法解决问题,还可以尝试查看测试日志输出和Spring的调试日志(通过设置logging.level.org.springframework=DEBUG)来获取更多关于Bean初始化过程的信息。如果问题仍然存在,可能需要更详细地查看你的测试代码和配置,检查是否有其他配置或代码影响了Spring的正常工作。
9. 检查是否指定了 TestExecutionListeners
如果没有使用 @SpringBootTest
,则需要手动开启Mockito的 Listener,执行依赖注入和reset操作,否则 @MockBean
注解的字段为 null。
- import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
- import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
- import org.springframework.test.context.ContextConfiguration;
- import org.springframework.test.context.TestExecutionListeners;
-
- @ContextConfiguration(classes = MyConfig.class)
- @TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
- class MyTests {
- // ...
- }
参考文档
7 Popular Unit Test Naming Conventions
Power Use of Value Objects in DDD
Spring boot Mocking and Spying Beans
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
这些都在我的软件测试学习交流群里:902061117 自取
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。