mockito(模拟测试)框架基本使用指南

简介: mockito(模拟测试)框架基本使用指南

概述

参考:Mockito单元测试的使用

介绍

  • Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,避免为了测试一个方法,却要自行构建整个 bean 的依赖链。

    同时 Mockito 也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。

  • 开发中有些依赖的接口还没有开发完成、有些接口还调不通等情况,但又想要验证部分代码逻辑时,可以使用 Mockito 对接口进行 mock。

    mock 可以理解为一个模拟对象,即一个替代者,可以替换掉依赖的对象,这样一来就可以把注意力集中在业务代码逻辑,验证代码的正确性。

  • 使用 mock 的好处是,可以对方法的入参和返回值做灵活的设置。

    比如可以在初始化的时候设置某个方法入参和返回值,这样当单元测试执行到这个方法的时候就不用受到方法依赖的约束。

    假如这样一种情况,想要测定时任务发邮件,其实想测的是这个定时任务逻辑是否正常,对发送邮件是否成功并不关心。由于发送邮件需要依赖其他系统,单元测试是发不了邮件的。这样把发送邮件的方法给 mock 掉就很有必要了。

  • 目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。


依赖引入

  • 方式1:spring-boot-starter-test 模块集成了 mockito

     <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
     </dependency>
  • 方式2:单独引入 mockito 依赖

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>3.3.3</version>
        <scope>test</scope>
    </dependency>


Mockito 的局限性

主要是两个:

  • 不能 mock 静态方法
  • 不能 mock 私有方法
  • 不能 mock final class

这两种方法可能只能是通过其他工具,或者通过上层方法调用来做测试。


快速入门

Mockito 的初始化

当要使用注解(比如 @Mock)来 mock 对象的使用,就需要先初始化 Mockito,这样用 @Mock 标注的对象才会被实例化,否则直接使用会报 Null 指针异常。

有两种初始化的方法:

  • 方式1:使用 MockitoAnnotations.initMocks() 方法

        @Mock
        private List mockList;
    
        @Before
        public void init(){
            MockitoAnnotations.initMocks(this);
        }
    
        @Test
        public void test(){
            mockList.add(1);
            verify(mockList).add(1);
        }
  • 方式2:在类上标注 @RunWith(MockitoJUnitRunner.class) 注解

    @RunWith(MockitoJUnitRunner.class)
    public class MockitoExample {
        @Mock
        private List mockList;
        @Test
        public void test(){
            mockList.add(1);
            verify(mockList).add(1);
        }
    }


Mock 的使用

mock 主要功能就是模拟一个对象出来,注意 Mock 出来的对象是假对象,对其任何方法的操作都不会真正执行的。

mock 模拟的对象可以理解为真实方法的一个代理,每次对方法的调用其实都是调用了代理方法,这个代理方法是一个空方法,不会做任何事情。方法的返回值都返回默认的:

  • boolean:返回 false
  • 基本数值类型:返回 0
  • 对象类型:返回 null


mock 有两种使用方法:

  • 直接代码 mock 一个对象

        // 直接代码mock一个List对象
        List list = Mockito.mock(ArrayList.class);
        // 这里的add操作其实没有真实调用
        list.add("22");
        // 打印其大小为0
        System.out.println(list.size());
        // 校验add("22")方法是否执行了,校验通过
        Mockito.verify(list).add("22");
  • 用 @Mock 造一个对象

        @Mock
        private List mockList;


使用示例:

public class MockExample {
    @Mock
    private Service01 service01;
    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
        // 如果加了这个返回值的设置,任何入参调用sendMail方法都是返回false
        Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
    }

    @Test
    public void testMock(){   
        // 这里直接返回了false,不会进去 sendMail 的代码逻辑
        boolean isSendSucc = service01.sendMail("张三","李四");
    }
}
public class Service01 {
    public boolean sendMail(String sender, String receiver){
        System.out.println(sender + "向" + receiver + "发送了一封邮件");
        return true;
    }
}


调用 mock 对象的方法

指定 mock 方法的返回值

常用大概有以下几种:

  • 方式1:调用完方法后指定返回值

    // 格式:
    Mockito.when(调用的类.方法).thenReturn(指定的返回值);
    
    // 示例:
    Mockito.when(service01.sendMail(Mockito.anyString(), Mockito.anyString())).thenReturn(false);

    如果是 @Mock 标注的对象方法,这样设置后不会进去方法执行,直接返回指定值。

    如果是 @Spy 标注的对象方法,这样设置后会进去执行方法,但是返回指定的返回值。

    注意

    • 指定方法的返回值时,入参的设置可以使用 matcher 做匹配,其中的 any 方法匹配任意值

      // 示例1:当使用任何整数值调用 userService 的 getUser() 方法时,就回传一个自定义 User 对象
      Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock"));
      User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
      User user2 = userService.getUserById(200); // 回传的user的名字也为I'm mock
      
      // 示例2:限制只有当参数的数字是 3 时,才会回传名字为 I'm mock 3 的 user 对象
      Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock"));
      User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
      User user2 = userService.getUserById(200); // 回传的user为null
      
      // 示例3:当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什么,都回传 100
      Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
      Integer i = userService.insertUser(new User()); //会返回100
    • 有多个入参的方法,一个入参使用了 matcher 做匹配,那么其他入参也要用 matcher 匹配。

      例如下面用了 any 方法,那么第二入参就不能写死了,可以用 eq 方法来做匹配

      // 错误的写法:
      Mockito.when(service01.sendMail(Mockito.anyString(), "王五")).thenReturn(true);
      // 正确的写法:
      Mockito.when(service01.sendMail(Mockito.anyString(), Mockito.eq("王五"))).thenReturn(true);
  • 方式2:直接返回指定值

    // 格式:
    Mockito.doReturn(方法返回值).when(spy标注的对象).调用的方法;
    
    // 示例:
    Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(), Mockito.anyString());

    这样设置后,不管是 @Mock 还是 @Spy 标注的对象方法都不会进去执行,会直接返回指定值。

  • 方式3:设置抛出异常

    // 格式:
    Mockito.when(调用方法).thenThrow(抛出的异常类);
    
    // 示例:
    Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenThrow(RuntimeException.class);


mock 返回值类型为 void 的方法

可以用 Mockito 的 doNothing()、doThrow() 和 doAnswer() 来对无返回值的函数进行 Mock 和验证。

  • doNothing()

    // 在 Mockito 中 doNothing()  是对无返回值的方法 mock 的默认行为。
    @Test
    public void whenAddCalledVerfied() {
        MyList myList = mock(MyList.class);
        doNothing().when(myList).add(isA(Integer.class), isA(String.class));
        myList.add(0, "");
      
        verify(myList, times(1)).add(0, "");
    }
    
    // 与上面的代码等价
    @Test
    public void whenAddCalledVerfied() {
        MyList myList = mock(MyList.class);
        myList(0, "");
      
        verify(myList, times(1)).add(0, "");
    }
  • doThrow()

    @Test(expected = Exception.class)
    public void givenNull_AddThrows() {
        MyList myList = mock(MyList.class);
        doThrow().when(myList).add(isA(Integer.class), isNull());
      
        myList.add(0, null);
    }
  • doAnswer()

    @Test
    public void whenAddCalledAnswered() {
        MyList myList = mock(MyList.class);
        doAnswer((Answer) invocation -> {
            Object arg0 = invocation.getArgument(0);
            Object arg1 = invocation.getArgument(1);
    
            assertEquals(3, arg0);
            assertEquals("answer me", arg1);
            return null;
        }).when(myList).add(any(Integer.class), any(String.class));
        myList.add(3, "answer me");
    }
  • Mockito 的 doCallRealMethod() 方法可以用在 void 函数上,允许调用原始对象的实际方法同时也允许去调用验证。

    @Test
    public void whenAddCalledRealMethodCalled() {
        MyList myList = mock(MyList.class);
        doCallRealMethod().when(myList).add(any(Integer.class), any(String.class));
        myList.add(1, "real");
      
        verify(myList, times(1)).add(1, "real");
    }


Spy 的使用

Spy 跟 Mock 不同之处在于,它是会真正执行方法逻辑的。相同之处是它可以指定方法的返回值。

有两种使用方法:

  • 直接代码实例化

        @Test
        public void mock2(){
            // 直接代码spy一个List对象
            List list = Mockito.spy(ArrayList.class);
            // 这里的add操作有真实调用
            list.add("22");
            // 打印其大小为1,是真实调用了的
            System.out.println(list.size());
            // 校验add("22")方法是否执行了,校验会通过
            Mockito.verify(list).add("22");
        }
  • 用 @Spy 标注

    注意:@Spy 不能标注接口,可以是实现类和抽象类

        @Spy
        private List spyList;


使用示例

    @Spy
    private Service01 service01;
    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
        // 如果加了这个返回值的设置,那么方法不会执行,任何入参调用sendMail方法都是返回false
        // Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(),Mockito.anyString());
    }

    @Test
    public void testMock(){
        // 这里会执行sendMail方法
        boolean isSendSucc = service01.sendMail("张三","李四");
        System.out.println("邮件发送是否成功:" + isSendSucc);
    }
    /**
    * 单元测试执行结果:
        张三向李四发送了一封邮件
        邮件发送是否成功:true
    */


InjectMocks 的使用

@InjectMocks 用来给标注的成员变量填充带有 @Mock 和 @Spy 标签的 bean,可以理解为它会吸取所有 @Mock 和 @Spy 标注的bean 为自己所用。

示例1:

bookService 用了 @IninjectMocks,那么 bookService 里面成员变量 bookDao 就会使用 @Mock 标注的 bookDao。

这样就解决了 bean 的依赖问题了,bookService 里面的 bookDao 的任何操作完全可以在单元测试类里指定返回值。

// 测试类
@RunWith(MockitoJUnitRunner.class)
public class BookControllerTest {
    @InjectMocks
    private BookServiceImpl bookService;
    @Mock
    private BookDao bookDao;

    @Before
    public void init(){
        Mockito.when(bookDao.getBookById("123")).thenReturn("《java语言》");
    }

    @Test
    public void testGetBook(){
        System.out.println(bookService.getBookById("123"));
    }
}

// service 接口
public interface BookService {
    String getBookById(String id);
}

// 接口实现类
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    @Override
    public String getBookById(String id) {
        return bookDao.getBookById(id);
    }
}

// dao 接口
@Mapper
public interface BookDao {
    String getBookById(String id);
}


示例2:

如果是嵌套的 bean 可以用 ReflectionTestUtils.setFileld() 绑定成员变量。

例如要测试 service 上层的 Controller,希望 mock bookDao,而不是 service:

// 测试类
@RunWith(MockitoJUnitRunner.class)
public class BookControllerTest {
    @Spy
    private BookServiceImpl bookService;
    @InjectMocks
    private BookController bookController;
    @Mock
    private BookDao bookDao;

    @Before
    public void init(){
        Mockito.when(bookDao.getBookById("123")).thenReturn("《java语言》");
        // 这里指定bookService的成员变量bookDao
        ReflectionTestUtils.setField(bookService,"bookDao",bookDao);
    }

    @Test
    public void testGetBook(){
        System.out.println(bookController.getBookById("123"));
    }
}

// Controller
@RestController
public class BookController {
    @Autowired
    private BookService bookService;
    
    @GetMapping("/getBook")
    public String getBookById(@RequestParam String id){
        return bookService.getBookById(id);
    }
}


@MockBean 的使用

@MockBean 是 SpringBoot 中增加的,用来支持容器中的 mock 测试。它跟 mock 的使用逻辑是一样,只是它修饰的对象是容器中的对象,也就是 bean 对象。

// 需要加载SpringBoot的上下文
@RunWith(SpringRunner.class)
@SpringBootTest
public class MockBeanExample {
    // 使用容器中对象,用MockBean
    @MockBean
    private Service01 service01;
    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
        // 如果加了这个返回值的设置,任何入参调用sendMail方法都是返回false
        // Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
    }

    @Test
    public void testMock(){   
        // 这里直接返回了true,会执行sendMail的代码
        boolean isSendSucc = service01.sendMail("张三","李四");
    }
}


// 注意这个类是容器中的组件
@Component
public class Service01 {
    public boolean sendMail(String sender, String receiver){
        System.out.println(sender + "向" + receiver + "发送了一封邮件");
        return true;
    }
}


@SpyBean 的使用

@SpyBean 也是 SpringBoot 增加的一个注解,用来支持 Spring 容器的单元测试,它与 Spy 的逻辑基本一致,不同之处就在于它标注的对象是容器对象。具体使用可以参考上面 @MockBean 的使用方法。


方法的校验和断言

通常写单元测试就是要断言方法的执行是否符合预期,除了 junit 提供的 Assert 类中的方法外,Mockito 也提供了几种校验方法:

  • 方法1:Mockito.verify() 方法断言方法是否被调用过

    // 格式:
    Mockito.verify(对象).对象的方法;
    
    // 示例1:校验list对象是否调用了add(“22”)方法
    Mockito.verify(list).add(“22”);
    // 示例2:检查调用 userService 的 getUserById()、且参数为3的次数是否为1次
    Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ;
    // 示例3:验证调用顺序,验证 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser() 方法
    InOrder inOrder = Mockito.inOrder(userService);
    inOrder.verify(userService).getUserById(3);
    inOrder.verify(userService).getUserById(5);
    inOrder.verify(userService).insertUser(Mockito.any(User.class));
  • 方法2:断言异常

        @Before
        public void init(){
            MockitoAnnotations.initMocks(this);
            // 让方法抛出异常
            Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString()))
                 .thenThrow(RuntimeException.class);
        }
    
        // 必须抛出指定的异常才会通过测试
        @Test(expected=RuntimeException.class)
        public void testThrowException(){
            service01.sendMail("张三","李四");
        }
  • 方法3:Assert 类中的断言方法


测试 Controller

这里总结了 2 种测试方法,都要在 SpringBoot 的框架中测试。一般在集成测试中使用,需要启动 Spring 容器。

  • 方式1:使用 @AutoConfigureMockMvc(推荐)

    @AutoConfigureMockMvc 会自动注入 MockMvc,可以方便的指定入参或者是 header

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class BookControllerTest2 {
        @Autowired
        public MockMvc mockMvc;
    
        @Test
        public void testGetBookInfo() throws Exception {
            MvcResult result = mockMvc.perform(
                    MockMvcRequestBuilders.post("/getBookInfo2").param("id","123").header("user","xiaoming"))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andReturn();
            System.out.println(result.getResponse().getContentAsString());
        }
    }
    
    
    @RestController
    public class BookController2 {
        @PostMapping("/getBookInfo2")
        public String getBookById(@RequestParam String id, @RequestHeader String user){
            System.out.println(user + "查询书籍信息,bookId=" + id);
            return "《java语言》";
        }
    }
  • 方式2:使用 TestRestTemplate 模板

    @RunWith(SpringRunner.class)
    //指定web环境,随机端口
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class BookControllerTest {
        //这个对象是运行在web环境的时候加载到spring容器中
        @Autowired
        private TestRestTemplate testRestTemplate;
    
        @Test
        public void testGetBookInfo(){
            String result = testRestTemplate.getForObject("/getBookInfo?id=123456", String.class);
            System.out.println(result);
        }
    }
    
    
    @RestController
    public class BookController2 {
        @GetMapping("/getBookInfo")
        public String getBookById(@RequestParam String id){
            System.out.println("查询书籍信息,bookId=" + id);
            return "《java语言》";
        }
    }


拓展

@RunWith 的使用

常见用法有以下几种:

  • @RunWith(SpringRunner.class) 或者 @RunWith(SpringJUnit4ClassRunner.class):

    代表在 Spring 容器中运行单元测试。如果配合 @SpringBootTest 就是在 SpringBoot 容器中运行单元测试。

    注:SpringRunner 就是 SpringJUnit4ClassRunner 的别名,它们作用是一样的。

    @SpringBootTest(classes = MyApplication.class)    // classes 加载启动类
    @RunWith(SpringRunner.class)
    public class BaseTest {
        public void runUnitTest(){
        }
    }
  • @RunWith(MockitoJUnitRunner.class)

    可以理解为使用 Mockito工作运行单元测试,它会初始化 @Mock 和 @Spy 标注的成员变量

  • @RunWith(Suite.class):代表是一个集合测试类,一般是如下用法,也就是其可一次性测试多个用例

    @RunWith(Suite.class)
    @Suite.SuiteClasses({ServiceTest.class, A.class})
    public class AllTest {
    }
    public class ServiceTest{
        @Test
        public void test01(){ }
       
        @Test
        public void test02(){}
    }
相关文章
|
14天前
|
测试技术 C# 数据库
C# 单元测试框架 NUnit 一分钟浅谈
【10月更文挑战第17天】单元测试是软件开发中重要的质量保证手段,NUnit 是一个广泛使用的 .NET 单元测试框架。本文从基础到进阶介绍了 NUnit 的使用方法,包括安装、基本用法、参数化测试、异步测试等,并探讨了常见问题和易错点,旨在帮助开发者有效利用单元测试提高代码质量和开发效率。
116 64
|
4天前
|
测试技术 Android开发 UED
探索软件测试中的自动化框架选择
【10月更文挑战第29天】 在软件开发的复杂过程中,测试环节扮演着至关重要的角色。本文将深入探讨自动化测试框架的选择,分析不同框架的特点和适用场景,旨在为软件开发团队提供决策支持。通过对比主流自动化测试工具的优势与局限,我们将揭示如何根据项目需求和团队技能来选择最合适的自动化测试解决方案。此外,文章还将讨论自动化测试实施过程中的关键考虑因素,包括成本效益分析、维护难度和扩展性等,确保读者能够全面理解自动化测试框架选择的重要性。
16 1
|
10天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
13天前
|
Web App开发 敏捷开发 存储
自动化测试框架的设计与实现
【10月更文挑战第20天】在软件开发的快节奏时代,自动化测试成为确保产品质量和提升开发效率的关键工具。本文将介绍如何设计并实现一个高效的自动化测试框架,涵盖从需求分析到框架搭建、脚本编写直至维护优化的全过程。通过实例演示,我们将探索如何利用该框架简化测试流程,提高测试覆盖率和准确性。无论你是测试新手还是资深开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
2天前
|
机器学习/深度学习 自然语言处理 物联网
探索自动化测试框架的演变与未来趋势
随着软件开发行业的蓬勃发展,软件测试作为保障软件质量的重要环节,其方法和工具也在不断进化。本文将深入探讨自动化测试框架从诞生至今的发展历程,分析当前主流框架的特点和应用场景,并预测未来的发展趋势,为软件开发团队选择合适的自动化测试解决方案提供参考。
|
5天前
|
测试技术 持续交付
探索软件测试中的自动化框架:优势与挑战
【10月更文挑战第28天】 随着软件开发的快速进步,自动化测试已成为确保软件质量的关键步骤。本文将探讨自动化测试框架的优势和面临的挑战,以及如何有效地克服这些挑战。
14 0
|
25天前
|
机器学习/深度学习 并行计算 数据可视化
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
这篇文章介绍了如何使用PaddleClas框架完成多标签分类任务,包括数据准备、环境搭建、模型训练、预测、评估等完整流程。
69 0
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
|
25天前
|
机器学习/深度学习 数据采集 算法
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
这篇博客文章介绍了如何使用包含多个网络和多种训练策略的框架来完成多目标分类任务,涵盖了从数据准备到训练、测试和部署的完整流程,并提供了相关代码和配置文件。
42 0
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
|
29天前
|
Web App开发 设计模式 测试技术
自动化测试框架的搭建与实践
【10月更文挑战第5天】本文将引导你理解自动化测试框架的重要性,并通过实际操作案例,展示如何从零开始搭建一个自动化测试框架。文章不仅涵盖理论,还提供具体的代码示例和操作步骤,确保读者能够获得实用技能,提升软件质量保障的效率和效果。
|
16天前
|
测试技术 开发者
探索软件测试中的自动化测试框架
在软件开发的世界中,质量是至关重要的。为了确保软件产品的质量,软件测试扮演着不可或缺的角色。本文将深入探讨自动化测试框架的概念、重要性以及如何有效地实施它们来提高软件测试的效率和效果。我们将从自动化测试的基本概念开始,逐步深入到不同类型的自动化测试工具和框架,最后探讨如何在实际项目中选择合适的自动化测试策略。