前言

Github:https://github.com/HealerJean

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

1、MOCK编程

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

两大目的

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

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

1.1、测试替身分类

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

image-20220329212107753

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

2.1、Mockito.mockmock出一个虚假的对象

@Test
public void test_1() {
  PersonDTO person = Mockito.mock(PersonDTO.class);

}

2.2、Mockito.verify

2.2.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.2.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)  

2.2.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);
}

2.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));
}

2.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();
}

2.5、Mockito.doReturn

指定返回特定值

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

2.6、mock 对象默认不调用&真实调用

2.6.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.6.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;
  }
}

3、注解

⬤ 使用@Mock@InjectMocks在没有spring上下文的情况下运行测试,这是首选,因为它要快得多

⬤ 使用@SpringBootTest @SpringMvcTest@MockBean 一起启动 spring上下文以创建模拟对象,@Autowired获取要测试的类的实例,模拟工具将用于其自动连接的依赖项。在为与数据库交互的代码编写集成测试或希望测试 REST API时,可以使用此选项。

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

3.1、非 Spring

SpringA -> B ->C,这是一个Spring 链路,这里只单侧1层 A -> B 可以用

3.1.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");
  }

}

3.1.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);
  }

}

3.2、Spring

SpringA -> B ->C,这是一个Spring 链路

3.2.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");
  }

}

3.2.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.3、Spring 项目内存初始化时候 mock

3.1.1、测试类

3.1.1.1、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
}


3.1.1.2、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);
    }
}

3.1.1.3、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 + "远程接口";
    }
}

3.1.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.1.3、BaseJunit5SpringTestImpl

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


}


4、解决方案

4.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>

4.2、mock 静态方法

一定要释放,否则会造成全局 mock

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

4.3、按照参数返回 mock 对象

ContactAuthor