原文鏈接
如果你想要在你的單測中嘗試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的方法
doAnswer
: 如果我們希望 mock的void方法去做一些事情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的指南,可以看一下下面的幾個鏈接: