草帽路飞UU 发表于 2022-9-1 16:50:20

你了解Mockito吗?

本帖最后由 草帽路飞UU 于 2022-9-1 16:53 编辑

前言

单元测试(UT)


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

Mockito和单元测试有什么关系?
与集成测试将系统作为一个整体测试不同,单元测试更应该专注于某个类。所以当被测试类与外部类有依赖的时候,尤其是与数据库相关的这种费时且有状态的类,很难做单元测试。但好在可以通过“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 {
如果用这个注解,跑单元测试的时候会加载SpringBoot的上下文,初始化Spring容器一次,显得格外的慢,这可能也是很多人放弃在Spring环境下使用单元测试的原因之一。
不过我们可以不用这个Spring环境,单元测试的目的应该是只测试这一个函数的逻辑正确性,某些容器中的相关依赖可以通过Mockito仿真。

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

@ExtendWith(MockitoExtension.class)
public class ListMockTest {
}

基本使用

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

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

控制台输出


null


当测试的单元依赖这个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));
    }
}


注意点

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

返回值为void
即使有些函数返回值为void,也可以使用存根。
//调用clear方法时,抛出异常doThrow(new RuntimeException()).when(mockedList).clear();
mockedList.clear();
连续存根
多次调用,返回不同的值。
    @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));    }返回值:
0
1
2
也可以简化为下面的这种写法,效果一样。

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

设置回调函数
调用某个函数的时候,执行一个回调函数。
    @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));    }哈哈哈,被我逮到了吧参数为:方法名为:get
结果由我决定
存根函数家族
除了上面出现的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());
    }


处理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"));
    }


正确的用法是:

    @Test
    public void mockStudent() {
      Student student = mock(Student.class);


      student.sleep(1, "1", "admin");


      verify(student).sleep(anyInt(), anyString(), eq("admin"));
    }

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;    }}
邮件有以下两种样式

public enum EmailStyle {
    HTML,DOC;
}

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

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


邮件平台代码如下:

public class DeliveryPlatform {


    public void deliver(Email email) {
      //do something
    }
}
现在我想验证一个问题,当我发送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());
    }
}

验证函数被调用的次数
下面的这个测试将不会通过
    @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());    }
报错如下:

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)

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

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


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


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

甚至支持不指定固定次数

//一次也不能调用,等于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");


创建mock对象的另一种方式:
@Mock
上述方法都是通过mock方法来构建仿真对象的,其实更简单的方法是通过注解。
    @Mock    private List mockedList;
    @Test    public void mockList() {      mockedList.add("one");      verify(mockedList).add("one");    }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");    }
当使用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);
    }

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

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

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

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

自动实例化 @InjectMocks

下面举一个比较常见的例子已有用户类
@Datapublic class UserInfo {    private String name;    private String password;
    public UserInfo(String name, String password) {      this.name = name;      this.password = password;    }}
有对应的服务以及数据存储接口

@Service
public class UserInfoService {


    @Autowired
    private UserInfoDao userInfoDao;


    public void printInfo() {
      UserInfo userInfo = userInfoDao.select();
      System.out.println(userInfo);
    }
}


public interface UserInfoDao {
    UserInfo select();
}

如果我要测试这个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();
    }
}

运行结果为:

UserInfo(name=admin, password=123)



页: [1]
查看完整版本: 你了解Mockito吗?