收款定制开发Mockito详细教程

前言

单元测试(UT)
收款定制开发工作一段时间后,收款定制开发才真正意识到代码质量的重要性。收款定制开发虽然囫囵吞枣式地开发,收款定制开发表面上看来速度很快,收款定制开发但是给后续的维护与拓收款定制开发展制造了很多隐患。
收款定制开发作为一个想专业但还不收款定制开发专业的程序员,收款定制开发通过构建覆盖率比较高的用例,收款定制开发可以比较显著地提高代码质量。收款定制开发如后续需求变更、收款定制开发版本迭代时,收款定制开发重新跑一次单元测试即收款定制开发可校验自己的改动是否正确。

Mockito收款定制开发和单元测试有什么关系?
收款定制开发与集成测试将系统作为一个整体测试不同,单元测试更应该专注于某个类。所以当被测试类与外部类有依赖的时候,尤其是与数据库相关的这种费时且有状态的类,很难做单元测试。但好在可以通过“”这种仿真框架来模拟这些比较费时的类,从而专注于测试某个类内部的逻辑。

SpringBoot与Mockito

spring-boot-starter-test中已经加入了Mockito依赖,所以我们无需手动引入。
另外要注意一点,在SpringBoot环境下,我们可能会用@SpringBootTest注解。

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@BootstrapWith(SpringBootTestContextBootstrapper.class)@ExtendWith({SpringExtension.class})public @interface SpringBootTest {
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果用这个注解,跑单元测试的时候会加载SpringBoot的上下文,初始化Spring容器一次,显得格外的慢,这可能也是很多人放弃在Spring环境下使用单元测试的原因之一。
不过我们可以不用这个Spring环境,单元测试的目的应该是只测试这一个函数的逻辑正确性,某些容器中的相关依赖可以通过Mockito仿真。

所以我们可以直接拓展自MockitoExtendsion,这样跑测试就很快了。

@ExtendWith(MockitoExtension.class)public class ListMockTest {}
  • 1
  • 2
  • 3

基本使用

mock与verify

import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.junit.jupiter.MockitoExtension;import java.util.List;import static org.mockito.Mockito.*;@ExtendWith(MockitoExtension.class)public class ListMockTest {    @Test    public void mockList() {        List mockedList  = mock(List.class);        mockedList.add("one");        mockedList.clear();        verify(mockedList).add("one");        verify(mockedList).clear();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

mock(List.class)会返回一个List的仿真对象,可以理解为“假对象”,要与后面提到的spy 区分开。
通过Mockito的verify来验证是否调用过List的add方法。

stubbing(存根)

什么是存根

注意:mocking和stubbing背后的理论很庞大。这里的解释只是针对于这个框架而言,比较粗浅。
上面通过mock函数得到了一个代理对象,调用这个对象的函数时,如果有返回值,默认情况下返回值都是null,如果基本类型,默认值是0或者false。

  @Test    public void mockList() {        List mockedList  = mock(List.class);        System.out.println(mockedList.get(0));    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

控制台输出

null
  • 1

当测试的单元依赖这个mock对象的返回值时,我们可以通过提前申明这个函数的返回值来测试各种各样的场景。
提前申明的这个过程被称为存根。

@ExtendWith(MockitoExtension.class)public class ListMockTest {    @Test    public void mockList() {        List mockedList  = mock(List.class);        //调用get(0)时,返回first        when(mockedList.get(0)).thenReturn("first");        //调用get(1)时,直接抛出异常        when(mockedList.get(1)).thenThrow(new RuntimeException());        //返回first        System.out.println(mockedList.get(0));        //抛出异常        System.out.println(mockedList.get(1));        //没有存根,则会返回null        System.out.println(mockedList.get(999));    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

注意点

  • 存根时可以被覆盖的(即对一种情况多次存根的话,以最后一次为准),但是不鼓励这么做,可读性会变差。
  • 一旦存根后,这个函数会一直返回这个值,不管你调用多少次。

返回值为void

即使有些函数返回值为void,也可以使用存根。

//调用clear方法时,抛出异常doThrow(new RuntimeException()).when(mockedList).clear();mockedList.clear();
  • 1
  • 2
  • 3
  • 4

连续存根

多次调用,返回不同的值。

    @Test    public void mockList() {        List mockedList  = mock(List.class);        when(mockedList.get(0)).thenReturn(0).thenReturn(1).thenReturn(2);        System.out.println(mockedList.get(0));        System.out.println(mockedList.get(0));        System.out.println(mockedList.get(0));    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

返回值:

012
  • 1
  • 2
  • 3

也可以简化为下面的这种写法,效果一样。

        when(mockedList.get(0)).thenReturn(0, 1, 2);
  • 1

设置回调函数

调用某个函数的时候,执行一个回调函数。

    @Test    public void mockList() {        List mockedList = mock(List.class);        when(mockedList.get(anyInt())).thenAnswer(new Answer<Object>() {            @Override            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {                System.out.println("哈哈哈,被我逮到了吧");                Object[] arguments = invocationOnMock.getArguments();                System.out.println("参数为:" + Arrays.toString(arguments));                Method method = invocationOnMock.getMethod();                System.out.println("方法名为:" + method.getName());                return "结果由我决定";            }        });        System.out.println(mockedList.get(0));    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

控制台打印:

哈哈哈,被我逮到了吧参数为:[0]方法名为:get结果由我决定
  • 1
  • 2
  • 3
  • 4

存根函数家族

除了上面出现的doReturn、doThrow、doAnswer外,还有:
doNothing() 啥也不干
doCallRealMethod() 调用真正的方法(不代理)

参数匹配器

基本用法

看完上面的存根,可能会有一个疑问:如果我想监控这个对象有没有被调用get方法,具体参数是什么我并不关心,该咋办。
这个时候就用到了参数匹配器。

    @Test    public void mockList() {        List mockedList  = mock(List.class);        when(mockedList.get(0)).thenReturn("first");        //返回first        System.out.println(mockedList.get(0));        //验证是否调用过get函数。这里的anyInt()就是一个参数匹配器。        verify(mockedList).get(anyInt());    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

处理anyInt(),还有很多的参数匹配器,默认的放在ArgumentMatchers类中。当然,也可以根据需求自定义参数匹配器或者使用hamcrest匹配器。
当一个函数接收多个参数时,如果其中有一个用了参数匹配器,那其他的参数也必须用。

    class Student{        public void sleep(int id, String studNo, String name) {        }    }    @Test    public void mockStudent() {        Student student = mock(Student.class);        student.sleep(1, "1", "admin");        verify(student).sleep(anyInt(), anyString(), eq("admin"));        verify(student).sleep(anyInt(), anyString(), eq("admin"));    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

正确的用法是:

    @Test    public void mockStudent() {        Student student = mock(Student.class);        student.sleep(1, "1", "admin");        verify(student).sleep(anyInt(), anyString(), eq("admin"));    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

ArgumentCaptor

当我们需要去验证函数外部的一些参数时,就需要用到这个。
以发送邮件为例
定义一个邮件类:

@Data@NoArgsConstructorpublic class Email {    private String to;    private String subject;    private String body;    private EmailStyle emailStyle;    public Email(String to, String subject, String body) {        this.to = to;        this.subject = subject;        this.body = body;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

邮件有以下两种样式

public enum EmailStyle {    HTML,DOC;}
  • 1
  • 2
  • 3

邮件服务会调用邮件平台发送邮件

public class EmailService {    private DeliveryPlatform deliveryPlatform;    public EmailService(DeliveryPlatform deliveryPlatform) {        this.deliveryPlatform = deliveryPlatform;    }    public void send(String to, String subject, String body, boolean html) {        EmailStyle emailStyle = EmailStyle.DOC;        if(html) {            emailStyle = EmailStyle.HTML;        }        Email email = new Email(to, subject, body);        email.setEmailStyle(emailStyle);        deliveryPlatform.deliver(email);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

邮件平台代码如下:

public class DeliveryPlatform {    public void deliver(Email email) {        //do something    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在我想验证一个问题,当我发送HTML邮件时,deliver这个函数收到的email到底是不是HTML类型的。
这种情况下,就可以通过ArgumentCaptor的方式来解决了。

@ExtendWith(MockitoExtension.class)public class EmailServiceTest {    @Mock    private DeliveryPlatform deliveryPlatform;    @InjectMocks    private EmailService emailService;    @Captor    private ArgumentCaptor<Email> emailArgumentCaptor;    @Test    public void testHtmlEmail() {        emailService.send("某人", "无题", "无内容", true);        verify(deliveryPlatform).deliver(emailArgumentCaptor.capture());        Email email = emailArgumentCaptor.getValue();        Assertions.assertEquals(EmailStyle.HTML, email.getEmailStyle());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

验证函数被调用的次数

下面的这个测试将不会通过

    @Test    public void mockList() {        List mockedList  = mock(List.class);        when(mockedList.get(0)).thenReturn("first");        //返回first        System.out.println(mockedList.get(0));        System.out.println(mockedList.get(0));        //验证是否被用过get        verify(mockedList).get(anyInt());    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

报错如下:

org.mockito.exceptions.verification.TooManyActualInvocations: list.get(<any integer>);Wanted 1 time:-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:43)But was 2 times:-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:39)-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:40)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

大概意思是,只希望这个函数被调用一次,但实际上被调用了两次。
可能有点懵,不过点进verify方法后就明白了,默认情况下只调用一次;

public static <T> T verify(T mock) {    return MOCKITO_CORE.verify(mock, times(1));}
  • 1
  • 2
  • 3

所以在调用的verify方法的时候,指定下调用次数即可。

verify(mockedList, times(2)).get(anyInt());
  • 1

甚至支持不指定固定次数

 //一次也不能调用,等于times(0) verify(mockedList, never()).add("never happened"); //至多、至少 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");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

创建mock对象的另一种方式:@Mock

上述方法都是通过mock方法来构建仿真对象的,其实更简单的方法是通过注解。

    @Mock    private List mockedList;    @Test    public void mockList() {        mockedList.add("one");        verify(mockedList).add("one");    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Spy(间谍)

介绍

上面讲的一些操作都是和Mock出来的对象相关的。通过mock()或者@Mcok注解标注的对象,可以理解为“假对象”。
Spy是针对于“真实存在”的对象。
在重构已有的旧代码时,Spy会比较好用。

    @Test    public void spyList() {        //申请了一个真实的对象        List list = new LinkedList();        List spy = spy(list);        //可以选择存根某些函数        when(spy.size()).thenReturn(100);        //调用真实的方法        spy.add("one");        spy.add("two");        //打印第一个元素        System.out.println(spy.get(0));        //获取list的大小        System.out.println(spy.size());        //验证        verify(spy).add("one");        verify(spy).add("two");    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

当使用spy的时候,有一个很容易掉进去的陷进。即spy监听的是真实的对象,在操作真实对象的时候可能会出现越界之类的问题。

    @Test    public void spyList() {        List list = new LinkedList();        List spy = spy(list);        //报错 IndexOutOfBoundsException, 因为这个List还是empty        when(spy.get(0)).thenReturn("foo");        //通过        doReturn("foo").when(spy).get(0);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注解

和@Mock类似,还可以用@Spy注解。

BDD(行为驱动开发)

针对比较流行的行为驱动开发,Mockito也提供了对应的支持:
如org.mockito.BDDMockito类中的given//when//then
BDD本文就不做拓展了,后续有时间再做梳理。

超时验证

如果要验证执行是否超时,可以这么做:

verify(student, timeout(1).times(1)).sleep(anyInt(), anyString(), eq("admin"));
  • 1

自动实例化 @InjectMocks

下面举一个比较常见的例子
已有用户类

@Datapublic class UserInfo {    private String name;    private String password;    public UserInfo(String name, String password) {        this.name = name;        this.password = password;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

有对应的服务以及数据存储接口

@Servicepublic class UserInfoService {    @Autowired    private UserInfoDao userInfoDao;    public void printInfo() {        UserInfo userInfo = userInfoDao.select();        System.out.println(userInfo);    }}public interface UserInfoDao {    UserInfo select();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如果我要测试这个service,并且不想和数据库有交互,那么可以创建一个UserInfoDao mock对象。
被测试类标注为@InjectMocks时,会自动实例化,并且把@Mock或者@Spy标注过的依赖注入进去。

@ExtendWith(MockitoExtension.class)public class UserInfoServiceTest {    @InjectMocks    private UserInfoService userInfoService;    @Mock    private UserInfoDao userInfoDao;    @Test    public void testPrint() {        UserInfo userInfo = new UserInfo("admin", "123");        when(userInfoDao.select()).thenReturn(userInfo);        userInfoService.printInfo();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

运行结果为:

UserInfo(name=admin, password=123)
  • 1

参考

本文大部分内容来自于官网,但不会完全照搬,只整理我认为可能用得到的地方。并且可能会用自己的语言重新组织一下,或者替换部分示例代码,望谅解。
官网地址:
参考博客1:
参考博客2:
如果您对其他语言的模拟也比较感兴趣,例如python,可以学习下面的博客:

如有错误,欢迎指正!

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发