一篇用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>(T 是Mock的對象被調用的方法的返回類型,在這個例子中這個T是一個布爾值)
  • 我們也可以定義一個對象去獲取when返回的對象
    OngoingStubbing<Boolean> stub = when(dao.save(customer));
  • 這個OngoingStubbing<T>對象提供了以下幾個非常有用的方法:
    • 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<Customer>() {
            @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<Void>() {
            @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<Customer> 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的指南,可以看一下下面的幾個鏈接:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章