前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、MOCK编程

单元测试中,一个重要原则就是不扩大测试范围,尽可能将 mock 外部依赖,例如外部的 RPC 服务、数据库等中间件。被 mock 的对象可以称作。

两大目的

1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等

2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

1、测试替身分类

「测试替身」,它来源于电影中的特技替身的概念。Meszaros 在他的文中[2]定义了五类替身。

image-20220329212107753

二、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.mockmock出一个虚假的对象

@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获取要测试的类的实例,模拟工具将用于其自动连接的依赖项。在为与数据库交互的代码编写集成测试或希望测试 REST API时,可以使用此选项。

注解 说哦名
@Mock 创建一个Mock,用于替换被测试类中的引用的 bean 或第三方类。
@InjectMocks 用于创建一个被测试类的实例,当您希望 Mockito 创建一个对象的实例,并使用带有@Mock注释的 mock 作为其依赖项时。
@Mockbean 可用于将模拟对象添加到 Spring 应用程序上下文。mock 将替换应用程序上下文中相同类型的任何现有 bean。如果没有定义相同类型的 bean,将添加一个新的bean。通常与@SpringBootTest一起使用

1、非 Spring

SpringA -> 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

SpringA -> 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)启动单侧

JavaJUnit测试框架中,@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、在使用 whenspy 对象的方法进行 stubbing(打桩)时,需要使用 doReturn...when 的形式,而不是普通的 when...thenReturn 形式,否则可能会出现一些意外的行为或报错。

@Spy
@InjectMocks
private Handler handler;


@Test
void testSaveVirtualFundDispenseDetail() {

    doReturn(context).when(handler).parentMethod(any());

    handler.testMethod(req);
}

4、快速单侧

此步骤旨在:消除需要覆盖的代码行的基数。如日常开发中的注解组件:lombokmapstruct 等,其注解会生成 class 文件。尤其是 lombok@Data@Builder 注解,一个 10 个字段的类其生成的 eaqualshash 方法就不止 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 类、EnumMybatis 生成的的ExampleWrapper 等类,这些类大多为 settergetter 方法,尤其是ExampleWrapper

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;
        }
    }
}

ContactAuthor