Mockito框架详解

Mockito

在这里插入图片描述

一、应用背景——Mock测试

1.Mock测试
应用场景

​ 开发过程中,当一个类或模块开发完成,需要进行单元测试,如果单元测试时遇到以下场景:

场景1:service层的代码中,包含对dao层的调用,但dao层代码尚未开发
场景2:web前端依赖后端接口返向数据进行连调测试,但后端接口并未开发完成
场景3:项目需要测试的模块中含有支付功能,但是项目还没有接入第三方支付

​ 当然我们等不及把所有的单元全部开发完成再测试…

这时候可以考虑:因为我们进行单元测试的目的只是验证这部分代码的功能逻辑没有问题就可以了,而对数据的真实性没有要求

那么这时候,我们可以来为未完成的部分构建出一个虚拟的对象或数据来完成测试,如下图在这里插入图片描述

定义

** Mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的比较复杂的对象,用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法,此方法解决了不同的单元之间由于耦合而难于开发、测试的问题 。**

2.常见Mock测试框架

1.EasyMock (https://mock.mengxuegu.com )

2.Mockito (https://github.com/mockito/mockito)

3.PowerMock (https://github.com/powermock/powermock.git )

4.Jmockit (http://jmockit.cn/index.htm)

二、Mockito概述

1.什么是Mockito?

在这里插入图片描述

官网:https://site.mockito.org/

2013年底,有机构对对30000个GitHub项目进行了统计分析,Mockita的使用量排名第4,超过如Guava或Spring。

2.Mockito有哪些优势?

Mockito官网描述语:

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors. Read more about features & motivations.(Mockito是一个仿真框架,它的味道非常好。它可以让你用一个干净简单的API编写漂亮的测试。Mockito不会让你宿醉,因为测试可读性很强,并且会产生干净的验证错误)

Mockito的创始人Dan North在2008年写下的一段话:

“We decided during the main conference that we should use JUnit 4 and Mockito because we think they are the future of TDD and mocking in Java” (我们在研讨会期间决定使用JUnit4和Mockito,因为我们认为它们是TDD和Java模拟的未来)

3.Mockito具体可以用来干什么?
3.1 verify interactions——验证交互行为(方法有没有按照预想的计划被调用)
import static org.mockito.Mockito.*;

// mock creation
List mockedList = mock(List.class);

// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one");
mockedList.clear();

// selective, explicit, highly readable verification
verify(mockedList).add("one");
verify(mockedList).clear();
3.2 stub method calls——存根方法调用(提前设定方法的返回值或事件)
// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");

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

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

三、功能体验

Step 1:关于依赖

一般项目需要先引入Juint和mockito-core依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.5.1</version>
    <scope>test</scope>
</dependency>

如果是SpringBoot项目,则在test起步依赖中已经存在这些依赖了,无需任何操作。

在这里插入图片描述

//可能需要手动进行以下静态导入
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.mockito.Mockito.*;

Step 2:畅玩莫吉托

1.创建一个仿真对象 mock()

共有三种方式,下面方式一、方式二原理都是一样的,走的同一个源代码

@Test
public void mockTest(){
    List mockList1 = mock(List.class);   //方式一
    List mockList2 = Mockito.mock(List.class);  //方式二
    System.out.println(mockList1.getClass());
    System.out.println(mockList2.getClass());
    //对比真实对象:
    System.out.println(new ArrayList<Object>().getClass());
}
执行结果:
	class org.mockito.codegen.List$MockitoMock$ZL8ZVAlj
	class org.mockito.codegen.List$MockitoMock$ZL8ZVAlj
	class java.util.ArrayList

方式三:通过@Mock注解生成

第一步: 在测试类上加  @RunWith(MockitoJUnitRunner.class)
第二步: 创建mock对象
	@Mock
    private BookService bookService;
2.验证交互 verify()

​ 用于验证交互是否按照预想的计划进行,如果方法未按计划被调用,则会报出异常并显示错误信息

2.1验证方法是否被调用
语法:verify(模拟对象, times(调用次数)).method(方法参数);
@Test
public void verifyTest1(){
    List mockedList = Mockito.mock(List.class);
    mockedList.get(0);
    verify(mockedList).get(0); //验证 mockedList 的.get(0)方法是否只被调用了 1 次
    verify(mockedList).get(1); //验证 mockedList 的.get(1)方法是否只被调用了 1 次
}
执行结果:
	Wanted but not invoked:
	list.get(1);
	However, there was exactly 1 interaction with this mock:
	list.get(0);
2.2 验证方法被调用的次数是否符合预期
语法:verify(模拟对象, times(预期次数)).method(方法参数);
@Test
public void verifyTest2(){
    List mockedList = Mockito.mock(List.class);
    mockedList.get(0);
    mockedList.get(0);
    mockedList.get(1);
    verify(mockedList, times(2)).get(0); //验证 mockedList 的.get(0)方法是否被调用了 2 次
    verify(mockedList, times(1)).get(1); //验证 mockedList 的.get(1)方法是否被调用了 1 次
}
	无任何运行结果,证明全都符合预期
2.3 不指定方法具体的被调用次数(至少多少次 / 至多多少次 / 从未调用)
语法:
	 verify(模拟对象, atLeast(至少几次)).method(方法参数); / verify(模拟对象, atLeastOnce()).method(方法参数);
	 verify(模拟对象, atMost(至少几次)).method(方法参数); / verify(模拟对象, atMostOnce()).method(方法参数);
     verify(模拟对象, never()).method(方法参数);
@Test
public void verifyTest3(){
    List mockedList = Mockito.mock(List.class);
    mockedList.get(0);
    mockedList.get(1);
    mockedList.get(1);
    verify(mockedList, atLeast(1)).get(0); //验证 mockedList 的.get(0)方法是否被调用了 2 次
    verify(mockedList, atMost(1)).get(1); //验证 mockedList 的.get(1)方法是否被调用了 1 次
    verify(mockedList, never()).get(2); //验证 mockedList 的.get(2)方法是否从未被调用
}
执行结果:	
	org.mockito.exceptions.verification.MoreThanAllowedActualInvocations: 
	Wanted at most 1 time but was 2
2.4 验证方法执行顺序
语法:
	1.创建顺序对象(可以为多个仿真对象同时验证执行顺序)
	InOrder inOrder = inOrder(仿真对象1, 仿真对象2, ...);
	2.验证是否按顺序执行
	inOrder.verify(仿真对象1).method1(参数1);
	inOrder.verify(仿真对象2).method2(参数2);
	......
@Test
public void verifyTest5(){
    List mockedList = Mockito.mock(List.class);
    mockedList.get(0);
    mockedList.get(1);
    mockedList.get(2);
    InOrder inOrder = inOrder(mockedList);  //为 mockedList 仿真对象创建 inOrder 对象
    //验证 mockedList 中的方法是否按以下顺序被调用
    inOrder.verify(mockedList).get(0);
    inOrder.verify(mockedList).get(1);
    inOrder.verify(mockedList).get(2);
}
2.5 参数匹配器
语法:verify(模拟对象, atLeast(至少几次)).method(参数表达式); 
@Test
public void verifyTest4(){
    List mockedList = Mockito.mock(List.class);
    mockedList.get(0);
    mockedList.get(1);
    mockedList.add("obj");
    verify(mockedList, atLeastOnce()).get(anyInt()); //验证 mockedList 的.get(任意int值)方法是否至少被调用1次
    verify(mockedList).add(eq("obj")); //验证 mockedList 的.add("obj")方法是否被调用了 1 次
}
	org.mockito.exceptions.verification.MoreThanAllowedActualInvocations: 
	Wanted at most 1 time but was 2

注意:如果你使用了参数匹配器,那么所有参数都应该使用参数匹配器 !!!

@Test
public void verifyTest5(){
   List mockedList = Mockito.mock(List.class);
   mockedList.add(1,"hello");
   verify(mockedList).add(anyInt(), anyString());      //正常
   verify(mockedList).add(anyInt(),"hello");   //会抛异常,因为第二个参数没有用参数匹配器
}
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:

This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(any(), eq("String by matcher"));
2.6验证是否存在冗余调用
语法:verifyNoMoreInteractions(仿真对象);   //验证是否存在方法调用后未被验证
@Test
public void verifyTest7(){
    List mockedList = mock(List.class);

    mockedList.add("one");
    mockedList.add("two");

    verify(mockedList).add("one");
    verifyNoMoreInteractions(mockedList);   //因为.add("two")调用了但是未验证,所以此次验证将会失败
}
3.打桩 stubbing()

提前申明方法调用触发事件,如:设定调用仿真对象某方法的返回值、抛异常、执行的动作等

为方法进行打桩的表达式叫做”测试桩函数“

设定好调用返回值或事件的方法叫做“测试桩”

通过编写测试桩函数制作测试桩的过程叫做”存根“,也叫“打桩”

3.1 方式一:when().thenXxx()
语法:when(对象.方法).thenXxx();
//thenAnswer()			执行回调函数
//thenCallRealMethod()	调用真正的方法(不代理)
//thenReturn()			返回值,仅针对有返回值的方法
//thenThrow()			抛出异常
@Test
public void stubTest(){
    List mockedList = mock(List.class);
    
    when(mockedList.get(7)).thenReturn(7);  //设定方法执行返回值
    
    System.out.println(mockedList.get(7));	//已经设置返回值为7
    System.out.println(mockedList.size());  //返回值类型为数字类型的方法,若未设置测试桩则默认返回 0
    System.out.println(mockedList.add(2));  //返回值类型为布尔类型的方法,若未设置测试桩则默认返回 false
    System.out.println(mockedList.get(3));  //返回值类型为引用类型的方法,若未设置测试桩则默认返回 null
}
//也可以设置连续测试桩:当调用次数超过预设的参数个数后,继续调用方法返回值为最后一个
@Test
public void stubTest(){
    List mockedList = mock(List.class);
    
    when(mockedList.get(7)).thenReturn(789);  //方式一
    //when(mockedList.get(7)).thenReturn(7).thenReturn(8).thenThrow(new RuntimeException());  //方式二
    
    System.out.println(mockedList.get(7));	//第一次调用返回值为7
    System.out.println(mockedList.get(7));	//第二次调用返回值为8
    System.out.println(mockedList.get(7));	//第三次调用返回值为9
    System.out.println(mockedList.get(7));	//第四次以后调用返回值依然为9
}
//回调函数,也可以用lambda表达式写
when(mockedList.get(666)).thenAnswer(new Answer<Object>() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        System.out.println("我是回调函数");
        System.out.println("你调用的方法是===>"+invocation.getMethod());
        System.out.println("你传入的参数是===>"+invocation.getArguments());
        return invocation.getArguments();
    }
});
System.out.println(mockedList.get(666));
3.2 方式二:doXxx().when()
语法:doXxx().when(仿真对象).method(参数);	
//doAnswer()			执行回调函数
//doCallRealMethod()	调用真正的方法(不代理)
//doNothing()			无操作
//doReturn()			返回值,仅针对有返回值的方法
//doThrow()				抛出异常
@Test
public void stubTest3() {
    List mockedList = mock(List.class);
    doThrow(new RuntimeException("hello")).when(mockedList).clear();
    
    //也可以设置连续调用桩
    doThrow(new RuntimeException("hello0000"))
        .doNothing().
   		.doThrow(new RuntimeException())
   		.when(mockedList).get(0);
    mockedList.clear();
    
    mockedList.get(0);//第一次调用报出异常
    mockedList.get(0);//第一次调用啥也没干
    mockedList.get(0);//第一次调用又报出异常
}
//回调函数
doAnswer(invocation -> {
            System.out.println("我是回调函数");
            System.out.println("你调用的方法是 ===>"+invocation.getMethod());
            System.out.println("你传入的参数是 ===>"+invocation.getArgument(0));
            return "返回值的类型随心所欲";
        }
).when(mockedList.get(666));
System.out.println(mockedList.get(666));
4.创建间谍对象 spy()

​ 上面讲的一些操作都是和Mock出来的对象相关的。通过mock()或者@Mcok注解标注的对象,可以理解为“假对象”。 Spy是针对于“真实存在”的对象。

spy和mock的不同:

  1. mock是虚拟除一个对象,spy则是创建一个真对象的副本。
  2. mock出的对象只能通过设置测试桩来预设交互动作,而spy可以调用真对象中的方法。所以在重构已有的旧代码时,Spy会比较好用。
/**
 * @Author: 一个普普通通的帅比
 * @CreateTime: 2022/11/4  19:46
 * @Description: 本类用于展示 spy 得到的“间谍”对象的特点
 * @Version: 1.0
 */
public class SpyTest {
   @Test
   public void spyTest(){
       Book book = new Book();
       Book spy = spy(book);

       spy.setId(10);
       spy.setName("hello");

       book.setDescription("我才是真的书");
       
       System.out.println(spy);
       System.out.println(book);
   }
}

//打印结果:
Book(id=10, type=null, name=hello, description=null)
Book(id=null, type=null, name=null, description=我才是真的书)

四、项目案例

需要单元测试的代码

​ 图书管理系统,现在要对Controller层代码进行单元测试,但Service层的代码还未开发完成。

/**
 * @Author: 一个普普通通的帅比
 * @CreateTime: 2022/9/29  20:32
 * @Description: 需要被测试的 BookController 代码
 * @Version: 1.0
 */
@RestController
@RequestMapping("/books")
public class BookController {
    
    @Autowired
    private BookService bookService;

    @PostMapping
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }

    @PutMapping
    public boolean update(@RequestBody Book book) {
        return bookService.update(book);
    }

    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable Integer id) {
        return bookService.delete(id);
    }

    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }

    @GetMapping
    public List<Book> getAll() {
        return bookService.getAll();
    }
}
Step0:引入Mockito依赖

​ 本项目不是基于SpringBoot框架开发的,所以需要先引入 Mockito 和 Junit 单元测试依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.5.1</version>
    <scope>test</scope>
</dependency>
Step1:为缺少的模块设置测试桩

​ 在BookController类中mock出一个BookService的仿真对象,并为其创建测试桩

/**
 * @Author: 一个普普通通的帅比
 * @CreateTime: 2022/11/2  23:32
 * @Description: 在已开发的 BookController 代码基础上,增加了通过 mock 创建的仿真 BookService对象
 * @Version: 1.0
 */
@RestController
@RequestMapping("/books")
public class TestBookController {

    //mock一个BookService仿真对象
    private BookService bookService = mock(BookService.class);

    /**
     * 新增图书功能
     * 测试桩设置:设定库中已经存在id=1的书,除了id=1这本书之外,都可以保存成功
     */
    @PostMapping
    public boolean save(@RequestBody Book book) {
        when(bookService.save(any())).thenReturn(true);
        when(bookService.save(new Book(1, null, null, null)))
                .thenReturn(false);

        return bookService.save(book);
    }

    /**
     * 图书信息更新编辑功能
     * 测试桩设置:当更新 id=100 这本书时,抛出"该图书不存在"异常,其他正常修改
     */
    @PutMapping
    public boolean update(@RequestBody Book book) {
        when(bookService.update(any())).thenReturn(true);
        when(bookService.update(new Book(100, null, null, null)))
                .thenThrow(new RuntimeException("该图书不存在"));

        return bookService.update(book);
    }

    /**
     * 根据指定 id 删除图书功能
     * 测试桩设置:设定 id=3 的书不存在,除 id=3 这本书,其他都能删除成功
     */
    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable Integer id) {
        when(bookService.delete(anyInt())).thenReturn(true);
        when(bookService.delete(3)).thenReturn(false);

        return bookService.delete(id);
    }

    /**
     * 根据 id 查询功能
     * 测试桩设置:设定任何 id 都能查询到对应图书
     */
    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        when(bookService.getById(anyInt()))
                .thenReturn(new Book(id, "畅销书", "java基础", "从入门到放弃"));

        return bookService.getById(id);
    }

    /**
     * 获取所有图书功能
     * 测试桩设置:直接返回提前定义好的 book列表
     */
    @GetMapping
    public List<Book> getAll() {
        List<Book> books = new ArrayList<>();
        books.add(new Book(1, "畅销书", "java基础", "从入门到放弃"));
        books.add(new Book(2, "历史书", "明朝那些事", "有趣的历史故事"));
        books.add(new Book(3, "任务传记", "苏东坡传", "一蓑烟雨任平生"));

        when(bookService.getAll()).thenReturn(books);

        return bookService.getAll();
    }
}
Step2:代码功能测试

编写测试类,对bookController层代码进行测试

/**
 * @Author: 一个普普通通的帅比
 * @CreateTime: 2022/11/2  23:56
 * @Description: 本类用于 BookController 的代码功能测试
 * @Version: 1.0
 */
public class ApplicationTest {
    private TestBookController testBookController = new TestBookController();
    @Test
    public void saveTest(){
        boolean save = testBookController.save(new Book(0, null, null, null));
        System.out.println(save);
    }

    @Test
    public void updateTset(){
        boolean update = testBookController.update(new Book(1,null,null,null));
        System.out.println(update);
    }

    @Test
    public void deleteTest(){
        boolean delete = testBookController.delete(2);
        System.out.println(delete);
    }

    @Test
    public void getByIdTest(){
        Book book = testBookController.getById(4);
        System.out.println(book);
    }

    @Test
    public void getAllTest(){
        List<Book> bookList = testBookController.getAll();
        System.out.println(bookList);
    }
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值