一篇用Mockito的指南

原文链接
如果你想要在你的单测中尝试Mockito,这篇文章可以告诉你如何注入Mock的对象,如何Mock方法,包括返回类型为void的方法。


我们平时接触的大多数类都需要依赖其他的类。很多时候,类中的方法都需要委托其他类中的方法来处理一些事情。如果我们只用Junit对特定类进行单元测试,那我们的测试方法也需要依赖这些方法。但是,我们希望我们在做单元测试时可以摆脱对其他类的依赖。

  • 例如我们想测试CustomerService中的addCustomer方法。但是,在addCustomer方法中调用了CustomerDao类中的save()方法。出于以下几点,我们不希望CustomerDao.save()的方法被调用:
    • 我们想单独测试addCustomer()方法中的逻辑
    • 我们可能还没有实现save()方法
    • 就算save()的方法存在一些错误,我们也不希望我们的单测失败
  • 所以我们需要mock那些被依赖的方法,这也是mocking测试框架的作用。
  • Mockito是我用来解决这些问题的工具。这篇文章我们会讨论如何有效地利用Mockito来Mock这些依赖。

如果,你对JUnit的单测一无所知的话,可以参考作者更早的一篇文章How to write greate unit tests with JUnit

什么是Mocktio


Mocktio 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.

-[“Mockito.” Mockito Framework Site. N.p., n.d. Web. 28 Apr. 2017.](https://site.mockito.org/)


用Mockito来注入Mock对象

下面让我们通过一个例子,看看我们怎么用Mocktio来去除单测中的依赖。在进行单元测试时,我们可以通过注入一个Mock的类来代替真正的实现。

public class CustomerService {
    @Inject
    private CustomerDao customerDao;
    public boolean addCustomer(Customer customer){
    if(customerDao.exists(customer.getPhone())){
         return false;
    }
         return customerDao.save(customer);
    }
    public CustomerDao getCustomerDao() {
         return customerDao;
    }
    public void setCustomerDao(CustomerDao customerDao) {
         this.customerDao = customerDao;
    }
}

下面使用Mockito来mockCustomerService中的依赖

public class CustomerServiceTest {
    @Mock
    private CustomerDao daoMock;
    @InjectMocks
    private CustomerService service;
    @Before
    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
    }
    @Test
    public void test() {
         //assertion here
    }
}

让我么来看看例子中各个注解的作用:

  • @Mock会为CustomerDao创建一个mock的实现
  • @InjectMocks注解的字段在实例化的时候,会将@Mock注解的对象注入到自己的实例中。
  • 那么这些对象是在什么时候创建的呢?他们是在setUp()函数中的MockitoAnnotations.initMocks(this);这一行被调用时,被创建的。
  • 该测试类的每个测试方法执行之前都会调用setUp()进行这些实例的初始化

用Mockito来Mock方法

到目前为止,一切都很顺利。接下去我们需要告诉Mock的对象,当特定的方法被调用的时候,它们该怎么做。
when then模式:

  • when(dao.save(customer)).thenReturn(true); 这一行代码会告诉Mockito框架,我们希望在这个mock的dao对象执行save()方法时,只需要我们传入对应的这个customer的实例,它就会返回true.
  • when是Mockito类的一个静态方法,它会返回一个OngoingStubbing(T 是Mock的对象被调用的方法的返回类型,在这个例子中这个T是一个布尔值)
  • 我们也可以定义一个对象去获取when返回的对象
    OngoingStubbing stub = when(dao.save(customer));
  • 这个OngoingStubbing对象提供了以下几个非常有用的方法:
    • thenReturn(returnValue)
    • thenThrow(exception)
    • thenCallRealMethod()
    • thenAnswer() - 这个方法可以用来设置一个更聪明的stub,也可以用来mock一些void的方法(参考see How to mock void method behavior)
  • 让我们再来看看这个方法when(dao.save(customer)).thenReturn(true);这里传进去的是一个实际存在的特定对象,有更好的写法吗?当然!我们可以用一个macher来替代实际的对象when(dao.save(any(Customer.class))).thenReturn(true);
  • 但是!!!!如果一个方法中有多个参数,我们不能混用实际对象和matcher。例如,下面这样的写法是 不允许的 !!
    Mockito.when(mapper.map(any(), "test")).thenReturn(new Something());
    这样写编译不会报错,但是执行的时候会抛出错误: matchers can't be mixed with actual values in the list of arguments to a single method.
  • 所以我们要么全部用matcher,要么全部用真实对象

让我们再来看看Mockito.when的用法:

package com.tdd;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class CustomerServiceTest {
    @Mock
    private CustomerDao daoMock;
    @InjectMocks
    private CustomerService service;
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
     @Test
        public void testAddCustomer_returnsNewCustomer() {
            when(daoMock.save(any(Customer.class))).thenReturn(new Customer());
            Customer customer = new Customer();
            assertThat(service.addCustomer(customer), is(notNullValue()));
        }
    //Using Answer to set an id to the customer which is passed in as a parameter to the mock method.
    @Test
    public void testAddCustomer_returnsNewCustomerWithId() {
        when(daoMock.save(any(Customer.class))).thenAnswer(new Answer() {
            @Override
            public Customer answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                if (arguments != null && arguments.length > 0 && arguments[0] != null){
                    Customer customer = (Customer) arguments[0];
                    customer.setId(1);
                    return customer;
                }
                return null;
            }
        });
        Customer customer = new Customer();
        assertThat(service.addCustomer(customer), is(notNullValue()));
    }
    //Throwing an exception from the mocked method
     @Test(expected = RuntimeException.class)
        public void testAddCustomer_throwsException() {
            when(daoMock.save(any(Customer.class))).thenThrow(RuntimeException.class);
            Customer customer = new Customer();
            service.addCustomer(customer);//
        }
}

用Mockito来mock void的方法

  1. doAnswer: 如果我们希望 mock的void方法去做一些事情
  2. doThrow:如果你想要mock的方法被调用时抛出一个异常,那么你可以用Mockito.doThrow()

下面会展示一个例子,这个例子并不是很好的示例。我只是想通过这个示例,让你向你展示基本用法。

@Test
    public void testUpdate() {
        doAnswer(new Answer() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                if (arguments != null && arguments.length > 1 && arguments[0] != null && arguments[1] != null) {
                    Customer customer = (Customer) arguments[0];
                    String email = (String) arguments[1];
                    customer.setEmail(email);
                }
                return null;
            }
        }).when(daoMock).updateEmail(any(Customer.class), any(String.class));
        // calling the method under test
        Customer customer = service.changeEmail("[email protected]", "[email protected]");
        //some asserts
        assertThat(customer, is(notNullValue()));
        assertThat(customer.getEmail(), is(equalTo("[email protected]")));
    }
    @Test(expected = RuntimeException.class)
    public void testUpdate_throwsException() {
        doThrow(RuntimeException.class).when(daoMock).updateEmail(any(Customer.class), any(String.class));
        // calling the method under test
        Customer customer = service.changeEmail("[email protected]", "[email protected]");
    }
}

用Mockito来测试void方法的两种方式

有返回值的方法可以通过对返回值进行断言来测试,但是一些返回类型为void的方法我们该怎么测试呢?我们测试的这个void方法可能会去调用其它的方法来完成它的工作,也可能只是处理以下传入的参数,也可能是用来产生一些数据,当然也可能是以上几种可能结合起来。不用担心,我们可以用Mockito来测试上面的所有场景。

Mockito中的Verify

Mocking又一个很棒的特性,再单元测试执行的过程中,我们可以mock对象中特定的方法被调用了几次。verify的方法有两个:

  • 一个只接受Mock的对象参数——如果一个方法只会被执行一次,我们一般会用这个方法
  • 另一个方法有两个参数,一个是Mock的对象,另一个是VerificationMode,Mockito中有几个方法可以提供一些常用的VerificationMode
    • times(int wantedNumberOfInvocations)
    • atLeast( int wantedNumberOfInvocations )
    • atMost( int wantedNumberOfInvocations )
    • calls( int wantedNumberOfInvocations )
    • only( int wantedNumberOfInvocations )
    • atLeastOnce()
    • never()

下面展示一个Mockito.verify的例子

package com.tdd;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class CustomerServiceTest {
    @Mock
    private CustomerDao daoMock;
    @InjectMocks
    private CustomerService service;
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void test() {
        when(daoMock.save(any(Customer.class))).thenReturn(true);
        Customer customer=new Customer();
        assertThat(service.addCustomer(customer), is(true));
        //verify that the save method has been invoked
        verify(daoMock).save(any(Customer.class));
        //the above is similar to :  verify(daoMock, times(1)).save(any(Customer.class));
        //verify that the exists method is invoked one time
        verify(daoMock, times(1)).exists(anyString());
        //verify that the delete method has never been  invoked
        verify(daoMock, never()).delete(any(Customer.class));
    }
}

捕获参数

Mockito的另外一个非常棒的特性就是ArgumentCaptor,通过ArgumentCaptor我们可以捕获到传入到Mock对象或者是spied的方法中的参数。不多说,看例子:

package com.service;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.dao.CustomerDao;
import com.entity.Customer;
public class CustomerServiceTest {
    @Mock
    private CustomerDao doaMock;
    @InjectMocks
    private CustomerService service;
    @Captor
    private ArgumentCaptor customerArgument;
    public CustomerServiceTest() {
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void testRegister() {
        //Requirement: we want to register a new customer. Every new customer should be assigned a random token before saving in the database.
        service.register(new Customer());
        //captures the argument which was passed in to save method.
        verify(doaMock).save(customerArgument.capture());
        //make sure a token is assigned by the register method before saving.
        assertThat(customerArgument.getValue().getToken(), is(notNullValue()));
    }
}

Spy和Mockito

为什么用Spy?

  • 有时候,我们希望真的去执行依赖的一些方法,但同时我们还是希望可以验证或者追踪依赖的方法。此时,就需要用到spy.
  • 一个字段如果带有@Spy注解,Mockito会给这个对象创建一个代理。因此,我们既可以调用真实的方法,同时也能对它进行交互验证。
  • 如果有需要的话,spy的对象的一些方法还是可以被mock的

下面,还是通过一个例子来看一下

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
public class CustomerServiceTestV2 {
    @Spy
    private CustomerDaoImpl daoSpy;
    @InjectMocks
    private CustomerService service;
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void test() {
        Customer customer = new Customer();
        assertThat(service.addCustomer(customer), is(false));
        verify(daoSpy).save(any(Customer.class));
        verify(daoSpy, times(1)).exists(anyString());
        verify(daoSpy, never()).delete(any(Customer.class));
    }
}

本文中的所有例子的源码都可以在这里找到
本文到这里就结束了,如果你想了解更多的Mockito的指南,可以看一下下面的几个链接:

  • Official Mockito website
  • Mockito Main reference documentation
  • Wiki page

你可能感兴趣的:(Java学习记录)