Mockito 单元测试

Base

单元测试,即称为单元,则是对一个代码块模块的测试,但代码块中调用了其他部分的方法我们并不想对其一起测试.所以需要将其方法进行模拟,当测试代码执行到该方法时,返回我们期望的值.这就是 Mockito 的意义.

WIKI 单元测试的意义

首先最直接的意义就是帮我们预防小BUG,一些边界值和基本逻辑是否正确都可以通过单测完成

而我认为最主要的意义是对重构的帮助,很多时候时候,一个很大的系统会导致很多人不敢改,因为不知道改了一个地方会不会导致其他地方出错。可以,一旦有了单元测试,开发人员可以很方便的重构代码,只要在重构之后跑一遍单元测试就可以知道是不是把代码“改坏了”

个人认为单元测试应该在定义完接口之后就完成,而不是在写完实现之后再去完成,因为在写完实现后思路可能会被限制住,导致在定义接口的时候考虑的某些情况没有进行实现,而如果有单测的话就不会出现这种问题了.

P.s. 单测是对自己代码负责任的体现.

Use it

以主要测试service方法为例

基本使用举例

主要就是使用Mock + 断言的方式进行测试逻辑是否正确 

service层测试

mock掉Dao层 并将其注入service层 可以Mock Dao层的方法

 private CustomerDao mockDao;
    private CustomerService customerService;

    @Before
    public void setup() {
        //mock出接口
        mockDao = Mockito.mock(CustomerDao.class);
        //将其注入
        customerService = new CustomerServiceImpl(mockDao);
    }

    @Test
    public void addCustomer() {
        Customer customer = new Customer();
        //mock掉方法
        Mockito.when(mockDao.add(customer)).thenReturn(1);
        //断言
        Assert.assertEquals(customerService.addCustomer(customer), new Integer(1));
    }

}

除此之外还可以使用注解Mock类: 

@RunWith(MockitoJUnitRunner.class) 
public class AdminServiceTest {

    @Rule //在使用注解的时候需要加上
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Mock // 定义 dao 层对象
    private AdminMapper adminMapper;
    @Mock
    private List list;
    @InjectMocks // 定义真实对象
    private AdminService adminService = new AdminServiceImpl();

    @Test
    public void testRegist() {
        Admin admin = new Admin();
        when(adminMapper.insert(any(Admin.class))).thenReturn(1); // 当依赖对象 adminMapper 的 insert 方法被测试对象 adminService 的 regist 方法调用的时候返回 1,其中 any(Admin.class) 是指 insert 的参数只要是 Admin 类就行
        assertTrue(adminService.regist(admin)); // 验证被测试对象 adminService 的 regist 方法调用后正常执行
        verify(adminMapper, times(1)).insert(any(Admin.class)); // 验证依赖对象 adminMapper 的 insert 方法确实被调用了一次, times(1) 指的就是调用了一次。
    }
}

有些方法想Mock定制,有些想调用真实方法

因为@Mock针对接口生成Mock类,所以我们是没法调用到真实的实现类的方法。可以使用@Spy注解标注属性,并且标注@Resource注解让Spring注入真实实现类,那么Mockito就会自动生成Spy类。

@Resource(name = "userService")  
privateUserService userService;  
  
@Spy  
@Resource  
privateUserDao userDao;  

Spy类就可以满足我们的要求。如果一个方法定制了返回值或者异常,那么就会按照定制的方式被调用执行;如果一个方法没被定制,那么调用的就是真实类的方法。

如果我们定制了一个方法A后,再下一个测试方法中又想调用真实方法,那么只需在方法A被调用前,调用Mockito.reset(spyObject);就行了。

还可以通过@Resource同时交由spring管理来使用没有Mock的类

@InjectMocks // 定义被测试类的对象
@Resource //同时交由spring管理 可以使用没有mock的注入类
private InsuranceBillCheckingService insuranceBillCheckingService;

 

Dao层测试(mybatis)

直接对Dao层进行测试 所以直接@Autowired

@RunWith(SpringRunner.class)
@SpringBootTest
public class TextMapperTest {
    @Autowired
    private TextMapper textMapper;

    @Test
    public void findByStaffId() {
        Employee employeeGet = textMapper.findByStaffId(2L);
        Employee employeeExpect = new Employee(); //2,2,'张三','15122222222','北京',1,1
        employeeExpect.setId(2L)
                .setStaffId(2L)
                .setName("张三")
                .setMobile("15122222222")
                .setArea("北京")
                .setGender(1)
                .setIsValid(1);
        Assert.assertTrue(employeeExpect.equals(employeeGet));
    }

}

@RunWith(SpringRunner.class) 

当一个类用@RunWith注释或继承一个用@RunWith注释的类时,JUnit将调用它所引用的类来运行该类中的测试而不是开发者去在junit内部去构建它

When a class is annotated with @RunWith or extends a class annotated with @RunWith, JUnit will invoke the class it references to run the tests in that class instead of the runner built into JUnit. We added this feature late in development. While it seems powerful we expect the runner API to change as we learn how people really use it. Some of the classes that are currently internal will likely be refined and become public.

@SpringBootTest

Spring Boot提供了一个@SpringBootTest注释,spring-test @ContextConfiguration当您需要Spring Boot功能时,它可以用作标准注释的替代方法。注释通过在测试中创建 ApplicationContext使用来实现SpringApplication。除了 @SpringBootTest提供许多其他注释之外,还提供了用于测试应用程序的更具体的AOP

P.s.不要忘记也添加@RunWith(SpringRunner.class)到您的测试中,否则注释将被忽略。

P.s.如果测试是@Transactional,则默认情况下会在每个测试方法结束时回滚事务。但是,当使用这种安排RANDOM_PORT或 DEFINED_PORT隐式提供真正的servlet环境时,HTTP客户端和服务器在不同的线程中运行,因此在单独的事务中运行。在这种情况下,在服务器上启动的任何事务都不会回滚。

参考自 SpringBoot Text 官方文档

有一篇有点过时的博客也写的不错

异常测试

对异常也可以进行测试 , 测试指定异常

    //测试异常
    @Test(expected = NullPointerException.class)
    public void addCustomerWithNull() {
        Customer customer = null;
        Mockito.when(mockDao.add(customer)).thenReturn(1);
        Assert.assertEquals(customerService.addCustomer(customer), new Integer(1));
    }

匹配器

Mockito 提供了匹配器进行参数的mock

@Test
    public void getCustomerList() {
        List customers = Lists.newArrayListWithCapacity(1);
        Mockito.when(mockDao.findAll(Mockito.anyInt(),Mockito.anyInt())).thenReturn(customers);
        Assert.assertEquals(customerService.getCustomerList(Mockito.anyInt(),Mockito.anyInt()), customers);
    }

但需要注意的是,一旦使用了匹配器,就必须入参都使用它,否则会报异常.

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
 //上面是正确的,因为eq返回参数匹配器

 verify(mock).someMethod(anyInt(), anyString(), "third argument");
 //上面将会抛异常,因为第三个参数不是参数匹配器,一旦使用了参数匹配器来验证,那么所有参数都应该使用参数匹配

Mock具体的类

@Test
public void testVerify() throws Exception {
    //mock creation a list
    List mockedList = mock(List.class);

    //using mock object
    mockedList.add("one");
    mockedList.add("two");
    mockedList.add("two");
    mockedList.clear();

    //verification
    //验证是否调用过一次 mockedList.add("one")方法,若不是(0次或者大于一次),测试将不通过
    verify(mockedList).add("one");
    //验证调用过2次 mockedList.add("two")方法,若不是,测试将不通过
    verify(mockedList, times(2)).add("two");
    verify(mockedList).clear();//验证是否调用过一次 mockedList.clear()方法,若没有(0次或者大于一次),测试将不通过
}

对于List.get()方法也可以进行mock,设置一些记录(stubbing)

@Test
public void testStubbing() throws Exception {
    //你可以mock具体的类,而不仅仅是接口
    LinkedList mockedList = mock(LinkedList.class);

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

    //打印 "first"
    System.out.println(mockedList.get(0));

    //这里会抛runtime exception
    System.out.println(mockedList.get(1));

    //这里会打印 "null" 因为 get(999) 没有设置
    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. Not convinced? See here.
    verify(mockedList).get(0);
}

验证准确的调用次数,最多、最少、从未等

使用verify来验证一些行为

@Test
public void testInvocationTimes() throws Exception {

    LinkedList mockedList = mock(LinkedList.class);

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

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

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

    //下面两个是等价的, 默认使用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");

    //从未调用过. never()是times(0)的别名
    verify(mockedList, never()).add("never happened");

    //用atLeast()/atMost()验证
    verify(mockedList, atLeastOnce()).add("three times");
    //下面这句将不能通过测试
    verify(mockedList, atLeast(2)).add("five times");
    verify(mockedList, atMost(5)).add("three times");
}

为void方法抛异常

@Test
public void testVoidMethodsWithExceptions() throws Exception {

    LinkedList mockedList = mock(LinkedList.class);
    doThrow(new RuntimeException()).when(mockedList).clear();
    //下面会抛RuntimeException
    mockedList.clear();
}

验证调用顺序

@Test
public void testVerificationInOrder() throws Exception {
    // A. Single mock whose methods must be invoked in a particular order
    List singleMock = mock(List.class);

    //使用单个mock对象
    singleMock.add("was added first");
    singleMock.add("was added second");

    //创建inOrder
    InOrder inOrder = inOrder(singleMock);

    //验证调用次数,若是调换两句,将会出错,因为singleMock.add("was added first")是先调用的
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");

    // 多个mock对象
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);

    //using mocks
    firstMock.add("was called first");
    secondMock.add("was called second");

    //创建多个mock对象的inOrder
    inOrder = inOrder(firstMock, secondMock);

    //验证firstMock先于secondMock调用
    inOrder.verify(firstMock).add("was called first");
    inOrder.verify(secondMock).add("was called second");
}

验证mock对象没有产生过交互

@Test
public void testInteractionNeverHappened() {
    List mockOne = mock(List.class);
    List mockTwo = mock(List.class);

    //测试通过
    verifyZeroInteractions(mockOne, mockTwo);

    mockOne.add("");
    //测试不通过,因为mockTwo已经发生过交互了
    verifyZeroInteractions(mockOne, mockTwo);
}

End

Mockito是一个优秀的用于单元测试的mock框架。Mockito已经在github上开源,详细请点击:https://github.com/mockito/mockito

除了Mockito以外,还有一些类似的框架,比如:

  • EasyMock:早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常
  • PowerMock:这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了
  • JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode

一般情况推荐使用Mockito

你可能感兴趣的:(Java基础)