[Java SE/Junit] 基于Java的单元测试框架Mockito

0 序

  • Mockito 是一个模拟测试框架,主要功能是在单元测试中模拟类/对象的行为。

1 为什么要使用Mockito?

Mock可以理解为创建一个虚假的对象,或者说模拟出一个对象.在测试环境中用来替换掉真实的对象,以达到我们可以

  • 验证该对象的某些方法的调用情况,调用了多少次,参数是多少.
  • 给这个对象的行为做一个定义,来指定返回结果或指定特定的动作.

2 Mockito数据隔离

根据 JUnit 单测隔离 ,当 Mockito 和 JUnit 配合使用时,也会将非static变量或者非单例隔离开。

比如使用 @Mock 修饰的 mock 对象在不同的单测中会被隔离开。

3 Mock方法

3.1 mock 对象:创建模拟对象

3.1.1 mock 实例对象/实例方法: mock(Class classToMock)

mock方法来自org.mockito.Mock,它标识可以mock一个对象或者是接口

public static <T> mock(Class<T> classToMock);
  • 入参:classToMock: 待mock对象的class类
  • 返回:mock出来的类的对象(此时对象内依旧无数据)
Random random = Mockito.mock(Random.class);

when(random.nextBoolean()).thenReturn(true);
logger.info("result: {}", random.nextBoolean());//result: true

3.1.2 mock 类对象/静态方法: mockStatic(T.class)

注:QueryFactory#query为静态方法

/**
 * 静态类 T、静态方法的 Mock
 * 1、mock方式: MockedStatic<T> mockClass = mockStatic(T.class); mockClass.when(...).thenReturn(...)
 * 2、静态类/静态方法需在多个测试用例中 mock 时,需使用: `try-with-resources`的方式,以解决此问题"To create a new mock, the existing static mock registration must be deregistered"
 */
try(MockedStatic<QueryFactory> mockedQueryFactory = Mockito.mockStatic(QueryFactory.class)){
	mockedQueryFactory.when( () -> QueryFactory.query(any(), any(), any(), any()) ).thenReturn(result);

	signalNamesMappingCacheService.init();
	try {
		commonSearchBizSevice.executeQuery(requestInfo);//内部将调用 QueryFactory.query(...) 
	} catch (Exception e) {
		logger.info("happens a exception: SUCCESS");//内置脚本反射调用失败
	}
}

3.2 Stub 存根 : 为mock对象规定预期的目标执行结果

简述

存根的意思就是给mock对象规定一行的行为,使其按照我们的要求来执行具体的动作。

样例程序

demo1

 //You can mock concrete classes, not just interfaces
 LinkedList mockedList = mock(LinkedList.class);

 //stubbing
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(0)).thenReturn("two");
 when(mockedList.get(1)).thenThrow(new RuntimeException());

 //following prints "two"
 System.out.println(mockedList.get(0));

 //following throws runtime exception
 System.out.println(mockedList.get(1));

 //following prints "null" because get(999) was not stubbed
 System.out.println(mockedList.get(999));

 //Although it is possible to verify a stubbed invocation, usually it's just redundant
 //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
 //If your code doesn't care what get(0) returns, then it should not be stubbed.
 verify(mockedList).get(0);

demo2

@Test
public void test(){
    // 创建Mock对象,参数可以是类或者接口
    List<String> list = mock(List.class);

    // 设置方法的预期返回值
    when(list.get(0)).thenReturn("zuozewei");
    when(list.get(1)).thenThrow(new RuntimeException("test exception"));

    String result = list.get(0);

    // 验证方法调用
    verify(list).get(0);

    //断言,list的第一个元素是否是 "zuozwei"
    Assert.assertEquals(result,"zuozewei");
}

常用方法

mock实例对象的常用方法

  • when(mockObject.actionMethod).thenReturn(String t) 设置方法的目标返回值
  • when(mockObject.actionMethod).thenThrow(Throwable... throwables) 让方法抛出异常
  • when(mockObject.actionMethod).thenAnswer(Answer answer) / then(Answer answer) 自定义方法处理逻辑
  • when(mockObject.actionMethod).thenCallRealMethod() 调用 spy 对象的真实方法

mock类对象的常用方法

  • mockStaticObject.when(Verification verification).thenReturn(...)
MockedStatic<QueryFactory> mockedQueryFactory = Mockito.mockStatic(QueryFactory.class)
mockedQueryFactory.when( () -> QueryFactory.query(any(), any(), any(), any()) ).thenReturn(result);

3.3 参数匹配:精确匹配与模糊匹配(anyXXX)

import org.junit.Assert;
import org.junit.Test;

import java.util.List;

import static org.mockito.Mockito.*;

public class MockitoDemo {

    @Test
    public void test() {
        List mockList = mock(List.class);

        Assert.assertEquals(0, mockList.size());
        Assert.assertEquals(null, mockList.get(0));

        mockList.add("a");  // 调用 mock 对象的写方法,是没有效果的

        Assert.assertEquals(0, mockList.size());      // 没有指定 size() 方法返回值,这里结果是默认值
        Assert.assertEquals(null, mockList.get(0));   // 没有指定 get(0) 返回值,这里结果是默认值

        when(mockList.get(0)).thenReturn("a");          // 指定 get(0)时返回 a

        Assert.assertEquals(0, mockList.size());        // 没有指定 size() 方法返回值,这里结果是默认值
        Assert.assertEquals("a", mockList.get(0));      // 因为上面指定了 get(0) 返回 a,所以这里会返回 a

        Assert.assertEquals(null, mockList.get(1));     // 没有指定 get(1) 返回值,这里结果是默认值

        // 参数匹配:精确匹配
        when(mockStringList.get(0)).thenReturn("a");
        when(mockStringList.get(1)).thenReturn("b");

        Assert.assertEquals("a", mockStringList.get(0));
        Assert.assertEquals("b", mockStringList.get(1));

        // 参数匹配:模糊匹配
        when(mockStringList.get(anyInt())).thenReturn("a");  // 使用 Mockito.anyInt() 匹配所有的 int
        
        Assert.assertEquals("a", mockStringList.get(0)); 
        Assert.assertEquals("a", mockStringList.get(1));
    }

    //eq(...)
    @Test
    public void test2(){
        ...
        when(databaseConnectionManager.borrowConnector(eq(DsType2.INFLUXDB), eq(dataSource), any(Integer.class))).thenReturn(connector);
        ...
    }
}

3.4 reset(mockObject) : 重置之前自定义的返回值和异常

import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.*;

public class MockitoDemo {
    static class ExampleService {
        public int add(int a, int b) {
            return a+b;
        }
    }

    @Test
    public void test() {
        ExampleService exampleService = mock(ExampleService.class);
        // mock 对象方法的默认返回值是返回类型的默认值
        Assert.assertEquals(0, exampleService.add(1, 2));
        // 设置让 add(1,2) 返回 100
        when(exampleService.add(1, 2)).thenReturn(100);
        Assert.assertEquals(100, exampleService.add(1, 2));
        // 重置 mock 对象,add(1,2) 返回 0
        reset(exampleService);
        Assert.assertEquals(0, exampleService.add(1, 2));
    }

    @Test
    public void test2() {
        ExampleService exampleService = spy(new ExampleService());
        // spy 对象方法调用会用真实方法,所以这里返回 3
        Assert.assertEquals(3, exampleService.add(1, 2));
        // 设置让 add(1,2) 返回 100
        when(exampleService.add(1, 2)).thenReturn(100);
        Assert.assertEquals(100, exampleService.add(1, 2));
        // 重置 spy 对象,add(1,2) 返回 3
        reset(exampleService);
        Assert.assertEquals(3, exampleService.add(1, 2));
    }
}

4 Mock 注解

@Mock 注解

@mock快速创建mock的方法,使用
@mock注解需要搭配MockitoAnnotations.openMocks(testClass)方法一起使用.

   public class ArticleManagerTest {

       @Mock 
       private ArticleCalculator calculator;
       @Mock 
       private ArticleDatabase database;
       @Mock 
       private UserProvider userProvider;

       private ArticleManager manager;

    
       @org.junit.jupiter.api.Test
       void testSomethingInJunit5() {
           // 初始化mock对象
           MockitoAnnotations.openMocks(testClass);
           //Mockito.mock(class);
           Mockito.spy(class);
       }

       // 简化
       // @BeforeEach
       // void setUp() {
       //     MockitoAnnotations.openMocks(this);
       // }
   
   }

@Spy注解 与 Spy方法

spy 和 mock不同,不同点是:

  • spy 的参数是对象实例,mock 的参数是 class。
  • 被 spy 的对象,调用其方法时默认会走真实方法。mock 对象不会。

@Spy注解需要搭配MockitoAnnotations.openMocks(testClass)方法一起使用.

import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.*;

class ExampleService {
    int add(int a, int b) {
        return a+b;
    }
}

public class MockitoDemo {

    // 测试 spy
    @Test
    public void test_spy() {
        ExampleService spyExampleService = spy(new ExampleService());
        // 默认会走真实方法
        Assert.assertEquals(3, spyExampleService.add(1, 2));
        // 打桩后,不会走了
        when(spyExampleService.add(1, 2)).thenReturn(10);
        Assert.assertEquals(10, spyExampleService.add(1, 2));
        // 但是参数比匹配的调用,依然走真实方法
        Assert.assertEquals(3, spyExampleService.add(2, 1));
    }

    // 测试 mock
    @Test
    public void test_mock() {
        ExampleService mockExampleService = mock(ExampleService.class);
        // 默认返回结果是返回类型int的默认值
        Assert.assertEquals(0, mockExampleService.add(1, 2));
    }
}

spy 对应注解 @Spy,和 @Mock 是一样用的。

import org.junit.Assert;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import static org.mockito.Mockito.*;

class ExampleService {
    int add(int a, int b) {
        return a+b;
    }
}

public class MockitoDemo {
    @Spy
    private ExampleService spyExampleService;

    @Test
    public void test_spy() {
        MockitoAnnotations.openMocks(this);
        Assert.assertEquals(3, spyExampleService.add(1, 2));
        when(spyExampleService.add(1, 2)).thenReturn(10);
        Assert.assertEquals(10, spyExampleService.add(1, 2));
    }
}

@InjectMocks : 注解注入 mock 对象

mockito 会将 @Mock、@Spy 修饰的对象自动注入到 @InjectMocks 修饰的对象中。
注入方式有多种,mockito 会按照下面的顺序尝试注入:

  • 构造函数注入
  • 设值函数注入(set函数)
  • 属性注入
package demo;
import java.util.Random;

public class HttpService {
    public int queryStatus() {
        // 发起网络请求,提取返回结果
        // 这里用随机数模拟结果
        return new Random().nextInt(2);
    }
}
package demo;

public class ExampleService {
    private HttpService httpService;
    public String hello() {
        int status = httpService.queryStatus();
        if (status == 0) {
            return "你好";
        }
        else if (status == 1) {
            return "Hello";
        }
        else {
            return "未知状态";
        }
    }
}
import org.junit.Assert;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.mockito.Mockito.when;


public class ExampleServiceTest {

    @InjectMocks // 将httpService主动注入
    private ExampleService exampleService = new ExampleService();

    @Mock
    private HttpService httpService;

    @Test
    public void test01() {
        MockitoAnnotations.initMocks(this);
        when(httpService.queryStatus()).thenReturn(0);
        Assert.assertEquals("你好", exampleService.hello());
    }
}

验证和断言: verify(...)方法

验证是校验待验证的对象是否发生过某些行为,Mockito中验证的方法是:verify

verify(mock).someMoethod("some arg");
verify(mock,times(100)).someMoethod("some arg");

使用verify验证(Junit的断言机制):

  @Test
  void check() {
    Random random = Mockito.mock(Random.class);
    System.out.println(random.nextInt());
    Mockito.verify(random,Mockito.times(2)).nextInt();
  }

Verify配合times()方法,可以校验某些操作发生的次数

 //using mock
 mockedList.add("once");

 mockedList.add("twice");
 mockedList.add("twice");

 mockedList.add("three times");
 mockedList.add("three times");
 mockedList.add("three times");

 // 默认调用1次
 verify(mockedList).add("once");
 verify(mockedList, times(1)).add("once");

 // 自定义调用多次
 verify(mockedList, times(2)).add("twice");
 verify(mockedList, times(3)).add("three times");

 // 从不调用
 verify(mockedList, never()).add("never happened");

 // atLeast()/atMost()  至少调用 / 之多调用
 verify(mockedList, atMostOnce()).add("once");
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("three times");
 verify(mockedList, atMost(5)).add("three times");

 // 超时验证
 verify(mock, timeout(100)).someMethod();
 verify(mock, timeout(100).times(1)).someMethod();

 //只要 someMethod() 在 100 毫秒内被调用 2 次,就会通过
 verify(mock, timeout(100).times(2)).someMethod();

 //someMethod() 至少在 100 毫秒内被调用 2 次,就会通过
 verify(mock, timeout(100).atLeast(2)).someMethod();

5 Mock测试场景

MockMVC 测试 (未亲测)

对于前后端分离的项目而言,无法直接从前端静态代码中测试接口的正确性,因此可以通过MockMVC来模拟HTTP请求。基于RESTful风格的SpringMVC的测试,可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试。

初始化MockMvc对象

@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;

//在每个测试方法执行之前都初始化MockMvc对象
@BeforeEach
public void setupMockMvc() {
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

接口测试

1 controller 层

/**
 * id:\\d+只匹配数字
 * @param id
 * @return
 */
@GetMapping("/user/{id:\\d+}")
public User getUserById(@PathVariable Long id) {

    return userService.getById(id);
}

2 Mockito构建自定义返回结果

userService.getById并没有返回结果,但是我们的测试并不关心userService.getById这个方法是否正常,只是在我们的测试中需要用到这个方法,所以我们可以Mock掉UserService的getById方法,自己定义返回的结果。

@MockBean
private UserService userService;

@Test
void getUserById() throws Exception {

    User user = new User();
    user.setId(1);
    user.setNickname("yunqing");
    //Mock一个结果,当userService调用getById的时候,返回user
    doReturn(user).when(userService).getById(any());

    //perform,执行一个RequestBuilders请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
    mockMvc.perform(MockMvcRequestBuilders
            //构造一个get请求
            .get("/user/1")
            //请求类型 json
            .contentType(MediaType.APPLICATION_JSON))
            // 期望的结果状态 200
            .andExpect(MockMvcResultMatchers.status().isOk())
            //添加ResultHandler结果处理器,比如调试时 打印结果(print方法)到控制台
            .andDo(MockMvcResultHandlers.print());
}

3 传参数

@Test
void getUserByUsername() throws Exception {
    // perform : 执行请求 ;
    mockMvc.perform(MockMvcRequestBuilders
            //MockMvcRequestBuilders.get("/url") : 构造一个get请求
            .get("/user/getUserByName")
            //传参
            .param("username","admin")
            // 请求type : json
            .contentType(MediaType.APPLICATION_JSON))
            // 期望的结果状态 200
            .andExpect(MockMvcResultMatchers.status().isOk());
}

4 期望返回结果集有两个元素

@Test
void getAll() throws Exception {
    User user = new User();
    user.setNickname("yunqing");
    List<User> list = new LinkedList<>();
    list.add(user);
    list.add(user);
    //Mock一个结果,当userService调用list的时候,返回user
    when(userService.list()).thenReturn(list);
    //perform,执行一个RequestBuilders请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
    mockMvc.perform(MockMvcRequestBuilders
            //构造一个get请求
            .get("/user/list")
            //请求类型 json
            .contentType(MediaType.APPLICATION_JSON))
            // 期望的结果状态 200
            .andExpect(MockMvcResultMatchers.status().isOk())
            //期望返回的结果集合有两个元素
            .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2))
            //添加ResultHandler结果处理器,比如调试时 打印结果(print方法)到控制台
            .andDo(MockMvcResultHandlers.print());
}

5 测试Post请求

@Test
void insert() throws Exception {

    User user = new User();
    user.setNickname("yunqing");
    String jsonResult = JSONObject.toJSONString(user);
    //直接自定义save返回true
    when(userService.save(any())).thenReturn(true);
    // perform : 执行请求 ;
    MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
            //MockMvcRequestBuilders.post("/url") : 构造一个post请求
            .post("/user/insert")
            .accept(MediaType.APPLICATION_JSON)
            //传参,因为后端是@RequestBody所以这里直接传json字符串
            .content(jsonResult)
            // 请求type : json
            .contentType(MediaType.APPLICATION_JSON))
            // 期望的结果状态 200
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print())
            .andReturn();//返回结果

    int statusCode = mvcResult.getResponse().getStatus();
    String result = mvcResult.getResponse().getContentAsString();
    //单个断言
    Assertions.assertEquals(200, statusCode);
    //多个断言,即使出错也会检查所有断言
    assertAll("断言",
            () -> assertEquals(200, statusCode),
            () -> assertTrue("true".equals(result))
    );

  • 常用API
//使用jsonPaht验证返回的json中code、message字段的返回值
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("00000"))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("成功"))

//body属性不为空
.andExpect(MockMvcResultMatchers.jsonPath("$.body").isNotEmpty())

// 期望的返回结果集合有2个元素 , $: 返回结果
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2));

Y 样例集合

Y.1 基于 Mockito 框架的测试类

Y.1.1 Code

import cn.johnny.bd.dataservice.biz.cansignal.service.SignalNamesMappingCacheService;
import cn.johnny.bd.dataservice.biz.dataservice.mapper.IQueryJobMapper;
import cn.johnny.bd.dataservice.common.debug.Print;
import cn.johnny.bd.dataservice.common.sqlTemplate.GlobalInlineVariableNamesEnum;
import cn.johnny.bd.dataservice.model.entity.SignalFieldMapping;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.AopTestUtils;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.*;

//@ExtendWith(MockitoExtension.class)

// MockitoJUnitRunner 为开发者提供了 Mockito 框架使用情况的自动验证,以及自动执行 initMocks()
@RunWith(MockitoJUnitRunner.class) // SpringJUnit4ClassRunner / SpringRunner / MockitoJUnitRunner

public class MockitoTest {
    private static final Logger logger = LoggerFactory.getLogger(MockitoTest.class);

//    @Mock
//    private String canParseMatrixConfigDBName; // [X] 无法 Mock final class

    /**
     * 使用 @Mock 注解封装需要被模拟调用的对象 | @Mock: 创建一个Mock.
     */
    @Mock
    private IQueryJobMapper queryJobMapper;

    @Mock
    private LoadingCache<String, Map<String, SignalFieldMapping>> signalNamesMappingCache;

    /**
     * 使用 @InjetcMock 注解是为了向里面添加 @Mock 注入的对象 | @InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
     */
    @InjectMocks
    private SignalNamesMappingCacheService cacheService;

    @Before
    public void setup(){
        ReflectionTestUtils.setField(cacheService, "canParseMatrixConfigDBName", "johnny_bdp");//final Class(String) 无法成为 Mock 对象。故通过这种 Spring 提供的 ReflectionTestUtils 工具注入该属性
//        ReflectionTestUtils.setField(cacheService, "queryJobMapper", queryJobMapper);
//        ReflectionTestUtils.setField(cacheService, "signalNamesMappingCache", signalNamesMappingCache);
    }

    /**
     * @description
     *  // step1  ready dataset for test stubs(准备测试桩所需的基础数据集)
     *  // step2 setup test stubs(设置测试桩)
     *  // step3 Run the target test class and method (执行测试目标类的目标方法)
     *  // step4 assert , verify and end over(断言、验证与结束)
     */
    @Test
    public void getSignalNamesMappingTestSuccess(){
        // step1  ready dataset for test stubs(准备测试桩所需的基础数据集)
        List<SignalFieldMapping> signalList = new ArrayList<>();
        SignalFieldMapping signalFieldMapping1 = new SignalFieldMapping(1, "bcm_pepsPowerMode", "bcm_peps_power_mode", "bcm_pepsPowerMode", "{\"0\":\"DEFAULT_POWERMODE\",\"1\":\"OFF_POWERMODE\",\"2\":\"ON_POWERMODE\",\"3\":\"RESERVE_POWERMODE\",\"4\":\"RESERVE_POWERMODE1\",\"5\":\"RESERVE_POWERMODE2\",\"6\":\"RESERVE_POWERMODE3\",\"7\":\" INVALID_POWERMODE\"}");
        SignalFieldMapping signalFieldMapping2 = new SignalFieldMapping(2, "bcm_pepsKeyNotInCarInd", "bcm_peps_key_not_in_car_ind", "Keyisnotincarindication.钥匙离开车内提示.", "{\"0\":\"NO_WARNING\",\"1\":\" WARNING\"}");
        signalList.add(signalFieldMapping1);
        signalList.add(signalFieldMapping2);

        CacheStats cacheStats = new CacheStats(1,1,1,1,1,1);

        String globalInlineVariableNamesCode = GlobalInlineVariableNamesEnum.SIGNAL_NAMES_ORIGIN_MAPPING.getCode();

        // step2 setup test stubs(设置测试桩)
        when(queryJobMapper.getSignalFieldMapping("johnny_bdp")).thenReturn(signalList);
        when(signalNamesMappingCache.stats()).thenReturn(cacheStats);

        // step3 Run the target test class and method (执行测试目标类的目标方法)
        cacheService.init();
        Map<String, SignalFieldMapping> signalFieldMappingMap = cacheService.getSignalNamesMapping(globalInlineVariableNamesCode);

        // step4 assert , verify and end over(断言、验证与结束)
        if(!signalFieldMappingMap.isEmpty()){
            logger.debug("signalFieldMappingMap is not empty, content as follows:");
            Print.print(signalFieldMappingMap);
        }

        Assert.assertNotNull(signalFieldMappingMap);
    }

    /**
     * 基本模拟测试1
     * @reference-doc
     *  [1] 走进Java接口测试之Mock(概念篇) - Tencent Cloud - https://cloud.tencent.com/developer/article/1465589
     */
    @Test
    public void mockitoBaseTest(){
        // 创建Mock对象,参数可以是类或者接口
        List<String> list = mock(List.class);

        // 设置方法的预期返回值
        when(list.get(0)).thenReturn("zuozewei");
        when(list.get(1)).thenThrow(new RuntimeException("test exception"));

        String result = list.get(0);
        logger.warn("result:{}", result);// zuozewei

        // 验证方法调用, 若有异常时会直接抛 Exception
        String verifyResult = verify(list).get(0);
        logger.warn("verifyResult:{}", verifyResult);//null

        //断言,list的第一个元素是否是 "zuozwei"
        Assert.assertEquals(result,"zuozewei");
    }

    /**
     * 基本模拟测试2
     */
    @Test
    public void mockBaseTest2(){
        //一旦mock对象被创建了,mock对象会记住所有的交互。然后你就可以选择性的验证感兴趣的交互
        //You can mock concrete classes, not only interfaces
        // 你可以mock具体的类型,不仅只是接口
        //创建 mock 对象,mock 1个 List 接口
//        List mockedList = mock(List.class);
        //若不使用静态导入,则:必须使用 Mockito 调用
//        List mockedList = Mockito.mock(List.class);
        LinkedList mockedList = mock(LinkedList.class);

        //stubbing
        // 测试桩
        when(mockedList.get(0)).thenReturn("first");
        when(mockedList.get(1)).thenThrow(new RuntimeException());

        //following prints "first"
        // 输出 “first”
        System.out.println(mockedList.get(0));

        //following prints "null" because get(999) was not stubbed
        // 因为get(999) 没有打桩,因此输出null
        System.out.println(mockedList.get(999));

        //Although it is possible to verify a stubbed invocation, usually it's just redundant
        //If your code cares what get(0) returns then something else breaks (often before even verify() gets executed).
        //If your code doesn't care what get(0) returns then it should not be stubbed. Not convinced? See here.
        // 验证 get(0) 被调用的次数
        verify(mockedList).get(0);

        // following throws runtime exception
        // 抛出异常 | 因 get(1) 的测试桩是抛出异常
        // System.out.println(mockedList.get(1));
    }

    /**
     * 基本模拟测试3
     */
    @Test
    public void mockBaseTest3(){
        // 生成 mock 对象
        Foo mockFoo = mock(Foo.class);

        // 测试桩
        when(mockFoo.bool(anyString(), anyInt(), any(Object.class))).thenReturn(true);

        // 验证/断言
        Assert.assertTrue(mockFoo.bool("A", 1, "A"));
        Assert.assertTrue(mockFoo.bool("B", 10, new Object()));

        // 测试桩
        when(mockFoo.bool(eq("false"), anyInt(), any(Object.class))).thenReturn(false);
        // 验证/断言
        Assert.assertFalse(mockFoo.bool("false", 10, new Object()));
    }
}


class Foo {
    boolean bool(String str, int i, Object obj) {
        return false;
    }

    int in(boolean b, List<String> strs) {
        return 0;
    }

    int bar(byte[] bytes, String[] s, int i) {
        return 0;
    }
}

Y.1.2 dependency

<!-- org.junit.runner.RunWith;org.junit.Assert;org.junit.Before;org.junit.Test -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<!-- 4.13.1 -->
	<version>${junit.version}</version>
	<scope>test</scope>
</dependency>

<!-- org.slf4j.Logger ; org.slf4j.LoggerFactory; -->
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<!-- 1.7.25 -->
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-log4j12</artifactId>
	<!-- 1.7.25 -->
</dependency>

<!-- 
 org.mockito.Mockito; org.mockito.MockedStatic; 
 org.mockito.junit.MockitoJUnitRunner; 
 org.mockito.InjectMocks; org.mockito.Mock;
 org.mockito.ArgumentMatchers.any; org.mockito.ArgumentMatchers.eq
 -->
<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-core</artifactId>
	<!-- 3.4.0 -->
	<version>${mockito.version}</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-inline</artifactId>
	<!-- 3.4.0 -->
	<version>${mockito.version}</version>
</dependency>
<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-junit-jupiter</artifactId>
	<!-- 3.4.0 -->
	<version>${mockito.version}</version>
	<scope>compile</scope>
	<exclusions>
		<exclusion>
			<artifactId>mockito-core</artifactId>
			<groupId>org.mockito</groupId>
		</exclusion>
	</exclusions>
</dependency>

<dependency>
	<groupId>com.github.jsonzou</groupId>
	<artifactId>jmockdata</artifactId>
	<!-- 4.3.0 -->
	<version>${jmockdata.version}</version>
	<scope>test</scope>
</dependency>

Y.2 基于 spring 框架的测试类

Y.2.1 Code

import cn.johnny.bd.dataservice.TestApplication;
import cn.johnny.bd.dataservice.biz.cansignal.service.SignalNamesMappingCacheService;
import cn.johnny.bd.dataservice.biz.dataservice.mapper.IQueryJobMapper;
import cn.johnny.bd.dataservice.common.debug.Print;
import cn.johnny.bd.dataservice.common.sqlTemplate.GlobalInlineVariableNamesEnum;
import cn.johnny.bd.dataservice.model.entity.SignalFieldMapping;
//import com.github.jsonzou.jmockdata.MockConfig;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.Map;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class SpringJUnitTest {
    private static final Logger logger = LoggerFactory
            .getLogger(SpringJUnitTest.class);

    @Resource
    private IQueryJobMapper queryJobMapper;

    @Resource
    private SignalNamesMappingCacheService cacheService;

    @Before
    public void init() {

    }

    @Test
    public void test(){
        String globalInlineVariableNamesCode = GlobalInlineVariableNamesEnum.SIGNAL_NAMES_ORIGIN_MAPPING.getCode();
        Map<String, SignalFieldMapping> signalFieldMappingMap = cacheService.getSignalNamesMapping(globalInlineVariableNamesCode);
        Print.print(signalFieldMappingMap);
        Assert.assertNotNull(signalFieldMappingMap);
    }
}

////////////// TestApplication  //////////////

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan({"cn.johnny"})
public class TestApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(TestApplication.class).web(WebApplicationType.NONE).run(args);
    }

}

Y.2.2 dependency

<!-- org.junit.runner.RunWith;org.junit.Assert;org.junit.Before;org.junit.Test -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<!-- 4.13.1 -->
	<version>${junit.version}</version>
	<scope>test</scope>
</dependency>

<!-- org.slf4j.Logger ; org.slf4j.LoggerFactory; -->
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<!-- 1.7.25 -->
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-log4j12</artifactId>
	<!-- 1.7.25 -->
</dependency>

<!-- org.springframework.boot.test.context.SpringBootTest -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-test</artifactId>
	<version>2.3.12.RELEASE</version>
	<!-- <scope>compile</scope> -->
</dependency>

<!-- org.springframework.test.context.junit4.SpringJUnit4ClassRunner; org.springframework.test.util.ReflectionTestUtils; -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<!-- 5.2.15.RELEASE -->
	<scope>test</scope>
</dependency>

K 模拟测试框架小结

mock-server: API/接口测试框架

Mokito

  • Mockito是mocking框架,它让你用简洁的API做测试。而且Mockito简单易学,它可读性强和验证语法简洁。
  • Mockito是GitHub上使用最广泛的Mock框架,并与JUnit结合使用。
  • Mockito框架可以创建和配置mock对象。
  • 使用Mockito简化了具有外部依赖的类的测试开发。

JMockData

小结

常用的mock框架有:EasyMock、JMock、Mockito、PowerMockito。比较常用的是Mockito。

比较项 EasyMock JMock Mockito PowerMockito
final方法 不支持 不支持 不支持 支持
private方法 不支持 不支持 不支持 支持
静态方法 不支持 不支持 支持 支持
SpringBoot依赖 实现较为复杂 实现较为复杂 默认依赖 基于Mockito扩展
API风格 略复杂 略复杂 简单 简单

Y FAQ for Mock

Q: 运行Mock测试用例方法时报InvalidTestClassError: Invalid test class 'com.xxx.demo.DemoApplicationTests' 1. No runnable methods

  • 问题描述

运行Mock测试用例方法时报:

...
org.junit.runners.model.InvalidTestClassError: Invalid test class 'com.xxx.demo.DemoApplicationTests':
  1. No runnable methods
...

又例如:

org.mockito.exceptions.base.MockitoException: 

No tests found in xxxPeriodicCollectMessageParseServiceImplTest
Is the method annotated with @Test?
Is the method public?


	at org.mockito.internal.runners.RunnerFactory.create(RunnerFactory.java:82)
	at org.mockito.internal.runners.RunnerFactory.createStrict(RunnerFactory.java:40)
	at org.mockito.junit.MockitoJUnitRunner.<init>(MockitoJUnitRunner.java:154)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1602)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1602)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
Caused by: java.lang.reflect.InvocationTargetException
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
	at org.mockito.internal.runners.util.RunnerProvider.newInstance(RunnerProvider.java:29)
	at org.mockito.internal.runners.RunnerFactory.create(RunnerFactory.java:75)
	... 22 more
Caused by: org.junit.runners.model.InvalidTestClassError: Invalid test class 'com.xxx.backend.service.periodiccollect.impl.xxxPeriodicCollectMessageParseServiceImplTest':
  1. No runnable methods
	at org.mockito.internal.runners.DefaultInternalRunner$1.<init>(DefaultInternalRunner.java:31)
	at org.mockito.internal.runners.DefaultInternalRunner.<init>(DefaultInternalRunner.java:30)
	... 26 more
  • 问题分析
  • 原因1: 可能是导错了@Test注解 (20250404 亲测)

应该使用org.junit.Test
而非使用org.junit.jupiter.api.Test

  • 原因2: 检查类、测试方法是否用public修饰
  • 参考文献

X 参考文献

posted @ 2022-12-15 15:18  千千寰宇  阅读(1638)  评论(0)    收藏  举报