单元测试之_Mockio
前言
Github:https://github.com/HealerJean
一、MOCK编程
单元测试中,一个重要原则就是不扩大测试范围,尽可能将
mock外部依赖,例如外部的RPC服务、数据库等中间件。被mock的对象可以称作。两大目的
1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
1、测试替身分类
「测试替身」,它来源于电影中的特技替身的概念。Meszaros 在他的文中[2]定义了五类替身。

二、Mockito
| 方法 | 说明 |
|---|---|
Mockito.mock(classToMock) |
模拟对象 |
Mockito.verify(mock) |
验证行为是否发生 |
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) |
触发时第一次返回value1,第n次都返回value2 |
Mockito.doThrow(toBeThrown).when(mock).[method] |
模拟抛出异常。 |
Mockito.mock(classToMock,defaultAnswer) |
使用默认Answer模拟对象 |
Mockito.when(methodCall).thenReturn(value) |
参数匹配 |
Mockito.doReturn(toBeReturned).when(mock).[method] |
参数匹配(直接执行不判断) |
Mockito.when(methodCall).thenAnswer(answer)) |
预期回调接口生成期望值 |
Mockito.doAnswer(answer).when(methodCall).[method] |
预期回调接口生成期望值(直接执行不判断) |
Mockito.spy(Object) |
用spy监控真实对象,设置真实对象行为 |
Mockito.doNothing().when(mock).[method] |
不做任何返回 |
Mockito.doCallRealMethod().when(mock).[method] |
调用真实的方法 |
Mockito.when(mock.[method]).thenCallRealMethod(); |
调用真实的方法,同上 |
reset(mock) |
重置mock |
1、Mockito.mock :mock出一个虚假的对象
@Test
public void test_1() {
PersonDTO person = Mockito.mock(PersonDTO.class);
}
2、Mockito.verify :
1)验证方法调用没(关心参数)&次数
@Test
public void test2_1() {
PersonDTO person = Mockito.mock(PersonDTO.class);
// 1、验证person的getSex得到了调用
person.getSex(1);
Mockito.verify(person).getSex(1);
Mockito.verify(person, Mockito.times(1)).getSex(1);
}
| 方法 | 说明 |
|---|---|
times(n) |
方法被调用n次 |
never() |
没有被调用 |
atLeast(n) |
至少被调用n次 |
atLeastOnce() |
至少被调用1次,相当于atLeast(1) |
atMost() |
最多被调用n次 |
2)验证方法调用没(不关系参数)
@Test
public void test_3() {
PersonDTO person = Mockito.mock(PersonDTO.class);
person.printing("healerjean");
// 1、只关心打印方法走没走,而不关心他的参数是什么的时候,我们就要用到Mock的any方法
Mockito.verify(person).printing(Mockito.anyString());
}
| 方法 | 说明 |
|---|---|
anyString() |
表示任何一个字符串都可以 |
anyInt |
|
anyLong |
|
anyDouble |
|
anyObject |
表示任何对象 |
any(clazz) |
表示任何属于clazz的对象 |
anyCollection |
|
anyCollectionOf(clazz) |
|
anyList(Map, set) |
|
anyListOf(clazz) |
3)Mockito.inOrder:验证调用顺序
@Test
public void test2_2(){
PersonDTO person = Mockito.mock(PersonDTO.class);
person.getSex(1);
person.isMan(1);
InOrder inOrder = Mockito.inOrder(person);
inOrder.verify(person).getSex(1);
inOrder.verify(person).isMan(1);
}
3、Mockito.when(xx).thenReturn(xx)
指定某个方法的返回值,或者是执行特定的动作
@Test
public void test_4_1() {
PersonDTO person = Mockito.mock(PersonDTO.class);
// 4.1、当调用person的isMan方法,同时传入"0"时,返回true
// (注意这个时候我们调用person.isMan(0);的时候值为true而调用其他数字则为false,
// 如果我们忽略数字,传任何值都返回true时,就可以用到我们上面讲的any()参数适配方法)
Mockito.when(person.isMan(0)).thenReturn(true);
// true
System.out.println(person.isMan(0));
// false
System.out.println(person.isMan(1));
//当调用person的isMan方法,同时传入"0"时,返回false,其他默认也都是 false
Mockito.when(person.isMan(0)).thenReturn(false);
// false
System.out.println(person.isMan(0));
// false
System.out.println(person.isMan(1));
Mockito.when(person.isMan(Mockito.anyInt())).thenReturn(true);
// true
System.out.println(person.isMan(0));
// true
System.out.println(person.isMan(1));
}
4、Mockito.doThrow:
指定某法方法抛出异常
@Test
public void test_4_2() {
List list = Mockito.mock(List.class);
list.add("123");
//1、当list调用clear()方法时会抛出异常
Mockito.doThrow(new RuntimeException()).when(list).clear();
list.clear();
}
5、Mockito.doReturn
指定返回特定值
- 适用于:
- spy 对象(部分 mock)
- void 方法(只能用
doNothing()/doThrow()/doAnswer()) - 避免真实方法被调用(尤其当方法有副作用时)
- 语法:先指定返回值,再指定方法
public void test_4_3() {
List list = Mockito.mock(List.class);
Mockito.doReturn("123").when(list).get(Mockito.anyInt());
System.out.println(list.get(0));
}
6、mock 对象默认不调用&真实调用
1)Mockito.doNothing():默认不调用
@Test
public void test_4_4(){
Foo foo = Mockito.mock(Foo.class);
//1、什么信息也不会打印, mock对象并不会调用真实逻辑
foo.doFoo();
//2、啥也不会打印出来
Mockito.doNothing().when(foo).doFoo();
foo.doFoo();
//不会调用真实逻辑,但是int默认值就是0,所以打印0
// 打印0
System.out.println(foo.getCount());
}
class Foo {
public void doFoo() {
System.out.println("method doFoo called.");
}
public int getCount() {
return 1;
}
}
2)Mockito.doCallRealMethod:真实调用
@Test
public void test_4_4(){
Foo foo = Mockito.mock(Foo.class);
//3、这里会调用真实逻辑, 打印出信息
Mockito.doCallRealMethod().when(foo).doFoo();
// 打印:"method doFoo called."
foo.doFoo();
Mockito.doCallRealMethod().when(foo).getCount();
// 打印 0
System.out.println(foo.getCount());
}
class Foo {
public void doFoo() {
System.out.println("method doFoo called.");
}
public int getCount() {
return 1;
}
}
7、Mockito.when(xx).thenAnswer(xx)
- 适用于:普通 mock 对象(mock)
- 语法:先调用方法,再指定返回值
when(demoPrcResource.rpcInvoke(anyString())).thenAnswer(DemoPrcResourceMock.rpcInvoke());
/**
* rpcInvoke
* @return String
*/
public static Answer<String> rpcInvoke() {
return invocation ->{
Object[] arguments = invocation.getArguments();
return "rpcInvoke";
};
}
| 特性 | when().thenReturn() |
doReturn().when() |
|---|---|---|
| 适用对象 | 仅 mock() 创建的对象 |
mock() 和 spy() 都可以 |
| 是否执行原方法 | ❌ 不执行(mock 默认无行为) | ❌ 不执行(安全) |
| 用于 void 方法 | ❌ 不行(编译错误) | ✅ 可以(配合 doNothing()) |
| 用于 spy 对象 | ⚠️ 危险!会先执行真实方法 | ✅ 安全,不会执行真实方法 |
| 语法可读性 | 更自然(像“当…则返回…”) | 稍绕(“返回…当…”) |
| 情况 | 推荐写法 |
|---|---|
| 普通 mock + 非 void 方法 | when(mock.method()).thenReturn(...) |
| spy 对象 | doReturn(...).when(spy).method() |
| void 方法 | doNothing().when(mock).method() |
| 避免真实方法执行(即使 mock) | doReturn(...).when(mock).method()(更安全) |
场景 1:普通 mock(两者都可用,推荐 when().thenReturn())
List<String> mockList = mock(List.class);
when(mockList.get(0)).thenReturn("hello"); // ✅ 推荐
// 也可以用 doReturn,但没必要
doReturn("hello").when(mockList).get(0); // ✅ 但不常用
场景 2:py 对象(必须用 doReturn!)
List<String> list = new ArrayList<>();
list.add("real");
List<String> spyList = spy(list);
// 危险!会先调用真实的 get(0),可能抛 IndexOutOfBoundsException!
// when(spyList.get(0)).thenReturn("mocked");
// 正确:不会调用真实方法
doReturn("mocked").when(spyList).get(0);
三、注解
⬤ 使用
@Mock和@InjectMocks在没有spring上下文的情况下运行测试,这是首选,因为它要快得多⬤ 使用
@SpringBootTest或@SpringMvcTest与@MockBean一起启动spring上下文以创建模拟对象,@Autowired获取要测试的类的实例,模拟工具将用于其自动连接的依赖项。在为与数据库交互的代码编写集成测试或希望测试RESTAPI时,可以使用此选项。
| 注解 | 说哦名 |
|---|---|
@Mock |
创建一个Mock,用于替换被测试类中的引用的 bean 或第三方类。 |
@InjectMocks |
用于创建一个被测试类的实例,当您希望 Mockito 创建一个对象的实例,并使用带有@Mock注释的 mock 作为其依赖项时。 |
@Mockbean |
可用于将模拟对象添加到 Spring 应用程序上下文。mock 将替换应用程序上下文中相同类型的任何现有 bean。如果没有定义相同类型的 bean,将添加一个新的bean。通常与@SpringBootTest一起使用 |
1、非 Spring
Spring:A->B->C,这是一个Spring链路,这里只单侧1层 A -> B 可以用
1)BaseJunit5MockitoTest
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* BaseJunit5MockitoTest
* @author zhangyujin
* @date 2023/3/23 17:34.
*/
@Slf4j
@ExtendWith(MockitoExtension.class)
public class BaseJunit5MockitoTest {
/**
* 所有测试方法运行前运行
*/
@BeforeAll
public static void beforeAll() {
log.info("[Junit5MockitoBaseTest#beforeAll] Run before all test methods run");
}
/**
* 每个测试方法运行前运行
*/
@BeforeEach
public void beforeEach() {
//增加改注解
log.info("[Junit5MockitoBaseTest#beforeEach] Run before each test method runs");
}
/**
* 每个测试方法运行完毕后运行
*/
@AfterEach
public void afterEach() {
log.info("[Junit5MockitoBaseTest#afterEach] Run after each test method finishes running");
}
/**
* 在所有测试方法运行完毕后运行
*/
@AfterAll
public static void afterAll() {
log.info("[Junit5MockitoBaseTest#afterAll] Run after all test methods have finished running");
}
}
2)Junit5MockitoTest
import com.healerjean.proj.BaseJunit5MockitoTest;
import com.healerjean.proj.service.service.CenterService;
import com.healerjean.proj.service.service.impl.TopServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* Junit5MockitoTest
* @author zhangyujin
* @date 2023/3/23 17:36.
*/
@Slf4j
public class Junit5MockitoTest extends BaseJunit5MockitoTest {
/**
* topService
*/
@InjectMocks
private TopServiceImpl topService;
@Mock
private CenterService centerService;
@DisplayName("Junit5MockitoTest.test")
@Test
public void test(){
when(centerService.centerMethod(anyString())).thenReturn("mockCenterMethod");
String result = topService.topMethod("HealerJean");
log.info("result:{}", result);
}
}
2、Spring
Spring:A->B->C,这是一个Spring链路
1)BaseJunit5SpringTest
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Junit5SpringBaseTest
* @author zhangyujin
* @date 2023/3/23 17:12.
*/
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = HljJunitApplication.class)
@DisplayName("Junit5-SpringBootTest 基础类")
public class BaseJunit5SpringTest {
/**
* 非静态方法必须指定 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
*/
@BeforeAll
public void beforeAll() {
//when(demoPrcResource.rpcInvoke(any())).thenReturn(DemoPrcResourceMock.rpcInvokeReturn());
}
/**
* 所有测试方法运行前运行
*/
// @BeforeAll
// public static void beforeAll() {
// log.info("[Junit5BaseTest#beforeAll] Run before all test methods run");
// }
/**
* 每个测试方法运行前运行
*/
@BeforeEach
public void beforeEach() {
log.info("[Junit5BaseTest#beforeEach] Run before each test method runs");
}
/**
* 每个测试方法运行完毕后运行
*/
@AfterEach
public void afterEach() {
log.info("[Junit5BaseTest#afterEach] Run after each test method finishes running");
}
/**
* 在所有测试方法运行完毕后运行
*/
@AfterAll
public static void afterAll() {
log.info("[Junit5BaseTest#afterAll] Run after all test methods have finished running");
}
}
2)Junit5SpringTest
import com.healerjean.proj.BaseJunit5SpringTest;
import com.healerjean.proj.service.service.BottomService;
import com.healerjean.proj.service.service.TopService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import javax.annotation.Resource;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* Junit5SpringTest
* @author zhangyujin
* @date 2023/3/23 17:23.
*/
@Slf4j
public class Junit5SpringTest extends BaseJunit5SpringTest {
/**
* topService
*/
@Resource
private TopService topService;
@MockBean
private BottomService bottomService;
@DisplayName("topService.topMethod")
@Test
public void test(){
when(bottomService.bottomMethod(anyString())).thenReturn("mockBottomMethod");
String result = topService.topMethod("HealerJean");
log.info("result:{}", result);
}
}
3、Spring 项目内存初始化时候 mock
1)测试类
a、DemoRpcProxy
package com.healerjean.proj.service.rpc.proxy;
/**
* DemoConsumer
*
* @author zhangyujin
* @date 2023/6/15 21:22.
*/
public interface DemoRpcProxy {
/**
* Rpc调用
*
* @param msg
* @return String
*/
String rpcInvoke(String msg);java
}
b、DemoRpcProxyImpl
package com.healerjean.proj.service.rpc.proxy.impl;
import com.healerjean.proj.service.rpc.DemoPrcResource;
import com.healerjean.proj.service.rpc.proxy.DemoRpcProxy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* DemoRpcProxy
*
* @author zhangyujin
* @date 2023/6/15 21:23.
*/
@Slf4j
@Service("demoRpcProxy")
public class DemoRpcProxyImpl implements DemoRpcProxy {
@Resource
private DemoPrcResource demoPrcResource;
/**
* Rpc调用
*
* @param reqString reqString
* @return String
*/
@Override
public String rpcInvoke(String reqString) {
return demoPrcResource.rpcInvoke(reqString);
}
}
c、DemoPrcResource
package com.healerjean.proj.service.rpc;
import org.springframework.stereotype.Service;
/**
* DemoPrcResource
*
* @author zhangyujin
* @date 2023/6/15 21:29.
*/
@Service("demoPrcResource")
public class DemoPrcResource {
/**
* rpcInvoke
*
* @return String
*/
public String rpcInvoke(String reqStr) {
return reqStr + "远程接口";
}
}
2)BaseJunit5SpringTest
package com.healerjean.proj.base;
import com.healerjean.proj.TomcatLauncher;
import com.healerjean.proj.mock.DemoPrcResourceMock;
import com.healerjean.proj.rpc.provider.DemoPrcResource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* Junit5SpringBaseTest
*
* @author zhangyujin
* @date 2023/3/23 17:12.
*/
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension.class)
@DisplayName("Junit5-SpringBootTest 基础类")
@SpringBootTest(classes = TomcatLauncher.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BaseJunit5SpringTest {
/**
* 1、使用@Resource 会有问题,不让 when
* 2、其他地方在使用的时候,用@Resource,不可以使用@MockBean了,因为会导致重复
*/
@MockBean
private DemoPrcResource demoPrcResource;
/**
* 非静态方法必须指定 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
*/
@BeforeAll
public void beforeAll() {
when(demoPrcResource.rpcInvoke(any())).thenReturn(DemoPrcResourceMock.rpcInvokeReturn());
}
/**
* 每个测试方法运行前运行
*/
@BeforeEach
public void beforeEach() {
}
/**
* 每个测试方法运行完毕后运行
*/
@AfterEach
public void afterEach() {
}
/**
* 在所有测试方法运行完毕后运行
*/
@AfterAll
public static void afterAll() {
}
}
3)Junit5SpringTestImpl
package com.healerjean.proj.base.impl;
import com.healerjean.proj.base.BaseJunit5SpringTest;
import com.healerjean.proj.rpc.consumer.proxy.impl.DemoRpcProxyImpl;
import com.healerjean.proj.rpc.provider.DemoPrcResource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.annotation.Resource;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* BaseJunit5SpringTest
*
* @author zhangyujin
* @date 2023/6/15 21:53.
*/
@Slf4j
public class Junit5SpringTestImpl extends BaseJunit5SpringTest {
/**
* demoRpcProxy
*/
@Resource
private DemoRpcProxyImpl demoRpcProxy;
@Resource
private DemoPrcResource demoPrcResource;
@DisplayName("BaseJunit5SpringTestImpl.test")
@Test
public void test1() {
String result = demoRpcProxy.rpcInvoke("success");
log.info("result:{}", result);
String result2 = demoRpcProxy.rpcInvoke("success");
log.info("result2:{}", result2);
}
@DisplayName("BaseJunit5MockitoTestImpl.test2")
@Test
public void test2() {
when(demoPrcResource.rpcInvoke(anyString())).thenReturn("test2MockMethod");
String result = demoRpcProxy.rpcInvoke("success");
log.info("result:{}", result);
String result2 = demoRpcProxy.rpcInvoke("success");
log.info("result2:{}", result2);
}
}
5、注意事项
1)启动单侧
在
Java的JUnit测试框架中,@ExtendWith和@RunWith是两个用于配置测试运行方式的注解,但它们适用于不同的JUnit版本,并且通常不会在同一测试类上同时使用。下面我将分别解释这两个注解的作用和区别,以及为什么它们通常不会一起使用。
a、@RunWith
@RunWith注解是JUnit 4中用于指定测试运行器的。测试运行器(Runner)是一个JUnit框架的扩展点,允许你完全控制测试的执行。通过指定一个自定义的运行器,你可以改变测试的行为,例如启用Mock对象的支持。在
JUnit中,如果你想要使用Mockito框架进行Mock对象的创建和管理,你可能会使用MockitoJUnitRunner。这个运行器会自动初始化用@Mock注解标记的字段,并在测试结束后清理它们。
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class PolicyChangeNewExtendPersistenceDbServiceImplTest {
@Mock
private MerchantPolicyService merchantPolicyService;
@InjectMocks
private PolicyChangeNewExtendPersistenceDbServiceImpl policyChangeNewExtendPersistenceDbService;
b、@ExtendWith
@ExtendWith注解是JUnit 5(也称为JUnit Jupiter)中引入的,用于扩展测试类的功能。与JUnit 4中的@RunWith相比,@ExtendWith更加灵活和强大,因为它允许你在测试类上注册一个或多个扩展(Extensions),这些扩展可以在测试的不同阶段(如初始化前后、测试方法执行前后等)插入自定义行为。
在JUnit 5中,如果你想使用 Mockito,你会使用MockitoExtension。这个扩展会自动处理用 @Mock 和 @InjectMocks注解标记的字段。
@ExtendWith(MockitoExtension.class)
public class MyTest {
// 测试代码
}
c、MockitoAnnotations.openMocks(this)
MockitoAnnotations.openMocks(this);是Mockito框架里的一个方法调用,其功能是对当前测试类里使用@Mock、@Spy、@Captor等注解标记的字段进行初始化。在JUnit测试中,借助这些注解可以方便地创建模拟对象,不过这些对象默认不会被初始化,需要调用MockitoAnnotations.openMocks(this)来完成初始化工作。
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
executor = new ActivityExecutor();
executor.setRedisService(redisService);
}
四、进阶
1、注入
1)变量入驻
mock不启动spring,当需要使用配置文件中的值可通过反射工具类进行配置
//参数说明:1.bean名称 2.需要set的属性名 3.需要set的值
ReflectionTestUtils.setField(businessTradeOrderServiceImplUnderTest, "popOrderType", 109);
//依赖pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
2)Service 注入
ReflectionTestUtils.setField(gwQuestionResourceMocks, "businessUserService", businessUserService);
2、mock 静态方法
一定要释放,否则会造成全局
mock
1)自动释放
try (MockedStatic<SpringUtils> springUtilsMockedStatic = mockStatic(SpringUtils.class)) {
when(dictionaryService.judgeDictDataExist(any(), any())).thenReturn(true);
springUtilsMockedStatic.when(() -> SpringUtils.getBean(service.class)).thenReturn(dictionaryService);
boolean res = dictionaryIncludedValidator.isValid("ProductComprehend", null);
Assertions.assertTrue(res);
}
2)手动释放
@Test
public void preHandleTest() throws IOException {
MockedStatic<EnvUtils> envUtilsMockedStatic = Mockito.mockStatic(EnvUtils.class);
MockedStatic<CookieUtils> cookieUtilsMockedStatic = Mockito.mockStatic(CookieUtils.class);
try {
envUtilsMockedStatic.when(EnvUtils::isTest).thenReturn(true);
cookieUtilsMockedStatic.when(()-> CookieUtils.getCookie(Mockito.anyString(), Mockito.any())).thenReturn("test");
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
afterInterceptor.preHandle(request, response, new Object());
}finally {
Optional.ofNullable(envUtilsMockedStatic).ifPresent(ScopedMock::close);
Optional.ofNullable(cookieUtilsMockedStatic).ifPresent(ScopedMock::close);
// cookieUtilsMockedStatic.close();
}
}
3、mock 普通方法
1)抽象父类
public abstract class AbstractTest {
@Resource
MyTestBean myTestBean;
protected String methodB(){
return myTestBean.getStringB();
}
}
@Test
public void testMethodB() {
MyTestBean myTestBean = Mockito.mock(MyTestBean.class);
when(myTestBean.getStringB()).thenReturn("B");
AbstractTest abstractTest = new AbstractTest() {
@Override
protected String methodB() {
return super.methodB();
}
};
ReflectionTestUtils.setField(abstractTest, "myTestBean", myTestBean);
assertEquals("B", abstractTest.methodB());
}
2)私有方法
@Slf4j
@Service
public class MyTestBean {
/**
* 这是一个私有方法
*
* @return String
*/
private String testGetStringA(String param) {
return param;
}
}
@InjectMocks
MyTestBean myTestBean;
@Test
public void getStringA() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = MyTestBean.class.getDeclaredMethod("getStringA", String.class);
method.setAccessible(true);
assertEquals("S", method.invoke(myTestBean, "S"));
}
3)同一类中的方法
1、使用
@Spy、注解来尝试mock父类方法parentMethod,@Spy创建的是一个部分mock对象,它会保留对象的真实行为,同时也允许你覆盖某些方法的行为。2、在使用
when对spy对象的方法进行stubbing(打桩)时,需要使用doReturn...when的形式,而不是普通的when...thenReturn形式,否则可能会出现一些意外的行为或报错。
@Spy
@InjectMocks
private Handler handler;
@Test
void testSaveVirtualFundDispenseDetail() {
doReturn(context).when(handler).parentMethod(any());
handler.testMethod(req);
}
4、快速单侧
此步骤旨在:消除需要覆盖的代码行的基数。如日常开发中的注解组件:
lombok、mapstruct等,其注解会生成class文件。尤其是lombok的@Data、@Builder注解,一个10个字段的类其生成的eaquals和hash方法就不止20行,内部builder类代码行数不下原类的3倍,同时生成的代码分支较多,较难覆盖,我的解决思路是排除。
1)lombok
解决方案:在项目根目录中,常见一个名为 lombok.config 的文件,配置如下:
## 声明根配置文件config.stopBubbling=true# 排除单测统计(起作用生成@Generated注解,避免jacoco扫描)lombok.addLombokGeneratedAnnotation=true
2)mapstruct
解决方案:通过升级 mapstruct 版本至1.3.1版本及以上。在 mapstruct 高版本中,生成的代码类上存在 @Generated 注解,该注解声明此类由程序自动生成,告诉 jacoco 在统计单测覆盖率或者其他审计工具进行审计时忽略该类。
3)自动覆盖
此步骤旨在:节省非核心代码的单测编写时间,让大家有更多时间聚焦更重要的事情。如
POJO类、Enum、Mybatis生成的的Example、Wrapper等类,这些类大多为setter、getter方法,尤其是Example和Wrapper类
package com.healerjean.proj.junit;
import com.google.common.collect.Lists;
import com.google.common.reflect.ClassPath;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class PojoCoverTest {
/**
* 需要覆盖的包集合
*/
private final static List<String> POJO_PACKAGE_LIST = Lists.newArrayList(
"com.healerjean.proj.service.domain"
);
/**
* 类加载器
*/
private ClassLoader classLoader = null;
@Before
public void before() {
// 获取当前类加载器
classLoader = Thread.currentThread().getContextClassLoader();
}
/**
* 反射执行所有:pojo、enum、exlpame 等基础domain类。
*/
@SuppressWarnings("all")
@Test
public void domainCoverTest() {
for (String packageName : POJO_PACKAGE_LIST) {
try {
// 加载指定包以及子包的类
ClassPath classPath = ClassPath.from(classLoader);
Set<ClassPath.ClassInfo> classInfos = classPath.getTopLevelClassesRecursive(packageName);
log.error(">>>>>>> domainCoverTest, packageName:{}, classSize:{}", packageName, classInfos.size());
// 覆盖单测
for (ClassPath.ClassInfo classInfo : classInfos) {
this.coverDomain(classInfo.load());
}
} catch (Throwable e) {
log.error(">>>>>>> domainCoverTest Exception package:{}", packageName, e);
}
}
}
private void coverDomain(Class<?> clazz) {
boolean canInstance = this.canInstance(clazz);
if (!canInstance) {
return;
}
// 枚举,执行所有值
if (clazz.isEnum()) {
Object[] enumList = clazz.getEnumConstants();
for (Object enumField : enumList) {
// 输出每一行枚举值
String enumString = enumField.toString();
}
}
// 执行外部类的所有方法
Object outerInstance = null;
try {
outerInstance = clazz.getDeclaredConstructor().newInstance();
this.method(clazz, outerInstance);
} catch (Throwable ignored) {
}
// 执行指定内部类的方法
for (Class<?> innerClass : clazz.getDeclaredClasses()) {
try {
boolean innerCanInstance = this.canInstance(clazz);
if (!innerCanInstance) {
continue;
}
boolean isStatic = Modifier.isStatic(innerClass.getModifiers());
Object innerClazzInstance = null;
if (isStatic) {
Constructor<?> constructor = innerClass.getDeclaredConstructor();
constructor.setAccessible(true);
innerClazzInstance = constructor.newInstance();
} else {
Constructor<?> constructor = innerClass.getDeclaredConstructor(clazz);
constructor.setAccessible(true);
innerClazzInstance = constructor.newInstance(outerInstance);
}
this.method(innerClass, innerClazzInstance);
} catch (Throwable ignored) {
}
}
}
private boolean canInstance(Class<?> clazz) {
int modifiers = clazz.getModifiers();
boolean isAnnotation = clazz.isAnnotation();
boolean isInterface = clazz.isInterface();
boolean isEnum = clazz.isEnum();
boolean isAbstract = Modifier.isAbstract(modifiers);
boolean isNative = Modifier.isNative(modifiers);
log.error(">>>>>>> coverDomain class:{}, isAnnotation:{}, isInterface:{}, isEnum:{}, isAbstract:{}, isNative:{}", clazz.getName(), isAnnotation, isInterface, isEnum, isAbstract, isNative);
if (isAnnotation || isInterface || isAbstract || isNative) {
return false;
}
// 如果是静态类或者final类,且不是枚举类也不处理
return isEnum || (!Modifier.isFinal(modifiers));
}
/**
* 通过反射调用指定实例的方法
*
* @param clazz 方法所属的类对象
* @param instance 方法所属的实例对象
*/
private void method(Class<?> clazz, Object instance) {
for (Method method : clazz.getDeclaredMethods()) {
if (!Modifier.isStatic(method.getModifiers())) {
method.setAccessible(true);
}
Class<?>[] parameterTypes = method.getParameterTypes();
try {
if (parameterTypes.length == 0) {
method.invoke(instance);
} else {
// null 值覆盖
try {
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> paramType = parameterTypes[i];
parameters[i] = this.getValue(paramType, true);
}
method.invoke(instance, parameters);
} catch (Throwable ignore) {
}
// 非 null 值覆盖
try {
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> paramType = parameterTypes[i];
parameters[i] = this.getValue(paramType, false);
}
method.invoke(instance, parameters);
} catch (Throwable ignore) {
}
}
} catch (Throwable ignored) {
}
}
}
/**
* 通过类的type 返回对应的默认值, 如果有其他类型请大家自行补充
*
* @param type 入参字段类型
* @return 返回对应字段的默认值
*/
private Object getValue(Class<?> type, boolean useNull) {
if (type.isPrimitive()) {
if (type.equals(boolean.class)) {
return false;
} else if (type.equals(char.class)) {
return '\0';
} else if (type.equals(byte.class)) {
return (byte) 0;
} else if (type.equals(short.class)) {
return (short) 0;
} else if (type.equals(int.class)) {
return 0;
} else if (type.equals(long.class)) {
return 0L;
} else if (type.equals(float.class)) {
return 0F;
} else if (type.equals(double.class)) {
return 0.0;
}
}
if (useNull) {
return null;
}
if (type.equals(String.class)) {
return "1";
} else if (type.equals(Integer.class)) {
return 1;
} else if (type.equals(Long.class)) {
return 1L;
} else if (type.equals(Double.class)) {
return 1.1D;
} else if (type.equals(Float.class)) {
return 1.1F;
} else if (type.equals(Byte.class)) {
return Byte.valueOf("1");
} else if (type.equals(List.class)) {
return new ArrayList<>();
} else if (type.equals(Short.class)) {
return Short.valueOf("1");
} else if (type.equals(Date.class)) {
return new Date();
} else if (type.equals(Boolean.class)) {
return true;
} else if (type.equals(BigDecimal.class)) {
return BigDecimal.ONE;
} else {
// 对于非原始类型和String,我们不提供默认值,即不传递参数
return null;
}
}
}


