前言
使用Mockito和JUnit5进行单元测试。
需要注入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.12.4</version> <scope>test</scope> </dependency>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency>
|
基础知识
- 使用@Test注解标记测试方法。
- 测试代码和被测试代码放到同一个包中。
示例背景
- 假设有一个服务类
MockService,其中有一个serviceMethod方法。
1 2 3
| public class MockService { public String serviceMethod(String name) ; }
|
- 假设有一个临时类
MockTmp,其中有一个tmpMethod方法。MockTmp的构造器需要传入一个MockService对象。
1 2 3 4 5 6 7 8
| public class MockTmp {
private final MockService mockService;
public MockTmp(MockService mockService);
public String tmpMethod(String name); }
|
- 假设有一个被测试类
MockUser,其中包含了MockService类、一个私有方法、一个静态方法、一个公有方法。
1 2 3 4 5 6 7 8 9 10 11 12
| public class MockUser {
private final MockService mockService;
public MockUser(MockService mockService);
private String privateMethod(String name) ;
public static String staticMethod(String name);
public String publicMethod(String name); }
|
测试将会覆盖:私有方法测试、静态方法测试、公有方法测试、构造器测试。
基本用法
创建Mock对象
- 使用
@Mock注解或Mockito.mock方法创建Mock对象。
- 使用
@Spy注解或Mockito.spy方法创建Mock对象。
- 使用
MockBean注解创建Mock对象。
- 使用
@InjectMocks注解或MockitoAnnotations.initMocks方法注入Mock对象。
1 2 3 4 5 6 7 8 9 10 11
| @Mock private MockService mockService;
@Spy @InjectMocks private MockUser mockUser;
@BeforeEach public void setUp(){ MockitoAnnotations.openMocks(this); }
|
注意:@Mock和@Spy注解的区别:
@Mock注解创建的对象是一个真正的Mock对象,它会覆盖被Mock的方法。
@Spy注解创建的对象是一个真实的对象,@Spy对象会保留原始实现,但会覆盖被Mock的方法。当希望mock被测试类中的部份方法时,可以使用@Spy注解。
Mock方法
- 使用
Mockito.when方法模拟方法调用。
- 使用
Mockito.doReturn方法模拟方法调用。
- 使用
Mockito.doThrow方法模拟方法调用。
- 使用
Mockito.doNothing方法模拟方法调用。
- 使用
Mockito.doAnswer方法模拟方法调用。
…
1 2 3 4 5 6 7 8
| Mockito.when(mockService.serviceMethod("Tom")).thenReturn("Hello, Tom!");
Mockito.doReturn("Hello, Tom!").when(mockService).serviceMethod("Tom");
Mockito.doThrow(new RuntimeException()).when(mockService).serviceMethod("Tom");
Mockito.doNothing().when(mockService).serviceMethod("Tom"); ...
|
verify方法和assert方法
- 使用
Mockito.verify方法验证方法调用。
- 使用
Assert.assertEquals方法断言方法调用。
- 使用
Assert.assertTrue方法断言方法调用。
…
1 2 3 4 5 6
| Mockito.verify(mockService, Mockito.times(1)).serviceMethod("Tom");
Assert.assertEquals("Hello, Tom!", mockService.serviceMethod("Tom"));
Assert.assertTrue("Hello, Tom!".equals(mockService.serviceMethod("Tom"))); ...
|
示例代码
测试私有方法
被测方法:
1 2 3 4 5
| private String privateMethod(String name) { System.out.println("Private Method Called"); name = name + " private"; return name; }
|
测试方法:
1 2 3 4 5 6 7 8 9 10
| @Test void privateMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { String name = "test"; Method method = mockUser.getClass().getDeclaredMethod("privateMethod", String.class); method.setAccessible(true); String result = (String) method.invoke(mockUser, name); assertEquals("test private", result); }
|
mockito无法直接模拟私有方法,需要通过反射调用。
测试静态方法
被测方法:
1 2 3 4 5
| public static String staticMethod(String name) { System.out.println("Static Method Called"); name = name + " static"; return name; }
|
测试方法:
1 2 3 4 5 6 7
| @Test void staticMethod() { String name = "test"; String result = MockUser.staticMethod(name); assertEquals("test static", result); }
|
mock静态方法
1 2 3 4 5
| try(MockedStatic<MockUser> mockUserStatic = Mockito.mockStatic(MockUser.class)){ mockUserStatic.when(() -> MockUser.staticMethod(anyString())).thenReturn("test"); String result = MockUser.staticMethod("test"); assertEquals("test", result); }
|
mock构造函数
1 2 3 4 5 6 7 8 9 10 11 12
| try(MockedConstruction<MockService> service = Mockito.mockConstruction(MockService.class, (mock, context) -> { Mockito.when(mock.serviceMethod(anyString())).thenReturn("test"); }); MockedConstruction<MockTmp> tmp = Mockito.mockConstruction(MockTmp.class, (mock, context) -> { Mockito.when(mock.tmpMethod(anyString())).thenReturn("test"); }) ){ MockService mockService = new MockService(); String result1 = mockService.serviceMethod("test"); assertEquals("test", result1); });
|
mock构造函数的时候,写在try小括号中和中括号中有什么区别?
-
try ( … ) { … } 小括号里的内容
• 小括号里的内容必须是 实现了 AutoCloseable 接口 的对象(比如 MockedConstruction、InputStream、Connection 等)。
• try 语句结束时(大括号执行完毕),这些对象会被 自动关闭(调用 close() 方法)。
• 在 Mockito 里,MockedConstruction 实现了 AutoCloseable,所以放在小括号里,会在 try 块结束时自动销毁构造函数 mock。
-
try { … } 大括号里的内容
• 大括号里就是普通的代码块,不会自动关闭资源。
• 如果你在大括号里写 MockedConstruction 的创建,它不会被自动关闭,你需要 手动调用 close()。
• 否则构造函数 mock 可能会“泄漏”,影响到后续测试。
测试公有方法
被调用的所有方法都mock掉,只测试公有方法。这样可以保证测试的是公有方法的逻辑,而不是依赖的其他方法。
被测方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public String publicMethod(String name) { try{ String name1 = mockService.serviceMethod(name); String name2 = privateMethod(name); String name3 = staticMethod(name); MockService mockService2 = new MockService(); String name4 = mockService2.serviceMethod(name); MockTmp mocktmp = new MockTmp(mockService2); String name5 = mocktmp.tmpMethod(name);
return name1 + " " + name2 + " " + name3 + " " + name4 + " " + name5; } catch (RuntimeException e){ throw new RuntimeException("Exception"); } }
|
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Test void publicMethod() { String name = "test"; Mockito.when(mockService.serviceMethod(anyString())).thenReturn("test");
try(MockedStatic<MockUser> mockUserStatic = Mockito.mockStatic(MockUser.class); MockedConstruction<MockService> service = Mockito.mockConstruction(MockService.class, (mock, context) -> { Mockito.when(mock.serviceMethod(anyString())).thenReturn("test"); }); MockedConstruction<MockTmp> tmp = Mockito.mockConstruction(MockTmp.class, (mock, context) -> { Mockito.when(mock.tmpMethod(anyString())).thenReturn("test"); }) ){
mockUserStatic.when(() -> MockUser.staticMethod(anyString())).thenReturn("test");
String result = mockUser.publicMethod(name); assertEquals("test test private test test test", result); } }
|
最后输出的结果是test test private test test test。
可以看出,只有privateMethod方法没有被mock掉,其他方法都被mock掉了。
目前还没有找到方法去mock类内私有方法(基于不能修改源代码、也不能使用powerMockito)。(如果有大佬看到,求教,邮箱1763605980@qq.com)
期望抛出异常
1 2 3 4 5 6
| @Test(expected = RuntimeException.class) public void publicMethodException() { String name = "test"; Mockito.doThrow(new RuntimeException()).when(mockService).serviceMethod(anyString()); assertThrows(RuntimeException.class, () -> mockUser.publicMethod(name)); }
|
doThrow() 与 thenThrow() 区别(用于 stubbing 有返回值方法)
- doThrow():用于 void 方法的 stubbing。
- thenThrow():用于有返回值的方法的 stubbing。
1 2 3 4
| doThrow(new RuntimeException()).when(mockObject).voidMethod();
when(mockObject.method()).thenThrow(new RuntimeException());
|
如果 源代码中有 try-catch 块,而在测试方法上使用了:
1
| @Test(expected = Exceptions.class)
|
那你根本无法覆盖 catch 中的内容,原因是:
JUnit 的 @Test(expected = ...) 会中断执行流程
当测试中抛出指定异常时,JUnit 会立即判定测试通过,不再执行后续逻辑:
- 如果源代码中 没有 catch,这个方式可以验证是否真的抛出了异常。
- 但如果源代码中 已经 catch 掉异常,异常就不会冒泡到测试层,
@Test(expected = ...) 就无效。
- 即使异常冒泡成功了,
catch 块内容也不会执行到,所以覆盖率里 catch 是“未执行”。
✅ 要覆盖 catch 中的代码,正确做法是:
不使用 @Test(expected = ...),而是让测试代码走完整流程,触发并验证 catch 块被执行。
1 2 3 4 5 6 7 8 9 10
| @Test public void testRemoveUser_withException() { when(userService.deleteUserById(1L)).thenThrow(new Exceptions("boom"));
controller.removeUser(1L);
}
|
注入@Value注解的值
使用反射
1 2 3 4 5
| @Before public void setUp() { MockitoAnnotations.openMocks(this); ReflectionTestUtils.setField(yourClassInstance, "yourFieldName", "yourValue"); }
|