前言
之前花了很多篇章在提手動建置測試替身,也從練習中了解為何要有測試替身和依賴注入等觀念,但手動建置有些問題存在,也顯得非常不方便,這些測試替身遲早會越來越多,相對地,我們光是維護就很耗時間,所以我們需要隔離框架(Isolation framework)來輔助我們,這也是接下來要提到的內容。手動建置測試替身的問題
- 使用手動建置測試替身會有下列問題:
- 建置測試替身很耗時間。
- 測試替身多了,也是要維護的。
- 若測試替身要保存多次呼叫的狀態時,你需要在測試替身實現相關程式碼。
- 很難在其他測試重複使用測試替身,可能基本程式碼可以使用,但是介面有超過三個以上方法需要實現時,維護就成了問題。
Mockito的使用
手動建置的測試替身就不再一一說明,先前都有足夠的例子可以練習,直接進入該如何使用Mockito來建置測試替身。首先,若要在Android Studio中使用Mockito框架,要先在gradle中加入Mockito,設定如下。dependencies {
testCompile 'org.mockito:mockito-core:2.2.28'
}
設定之後,我們都知道測試替身的用意,而使用Mockito來建置Mock物件有兩種做法,一個是利用mock這個方法(即使是Stub物件也是利用mock方法),另一種是@Mock這個Annotation,先介紹前者,這邊會以先前的FileParser和Bookstore來做練習。
以FileParser為例:mock方法
- FileParserWithMockitoTest測試程式
public class FileParserWithMockitoTest {
@Test
public void testNameShorterCharactersIsValidEvenWithSupportedExtension() throws Exception {
// Arrange
IExtensionManager stubExtensionManager = Mockito.mock(IExtensionManager.class);
Mockito.when(stubExtensionManager.isValid(Mockito.anyString())).thenReturn(true);
FileParser fileParser = new FileParser(stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsValidEvenWithSupportedExtension() throws Exception {
// Arrange
IExtensionManager stubExtensionManager = Mockito.mock(IExtensionManager.class);
Mockito.when(stubExtensionManager.isValid(Mockito.anyString())).thenReturn(true);
FileParser fileParser = new FileParser(stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(true));
}
@Test
public void testNameShorterCharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
IExtensionManager stubExtensionManager = Mockito.mock(IExtensionManager.class);
Mockito.when(stubExtensionManager.isValid(Mockito.anyString())).thenReturn(false);
FileParser fileParser = new FileParser(stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
IExtensionManager stubExtensionManager = Mockito.mock(IExtensionManager.class);
Mockito.when(stubExtensionManager.isValid(Mockito.anyString())).thenReturn(false);
FileParser fileParser = new FileParser(stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testVerifyIExtensionManagerCalledOneTimes() throws Exception {
// Arrange
IExtensionManager stubExtensionManager = Mockito.mock(IExtensionManager.class);
Mockito.when(stubExtensionManager.isValid(Mockito.anyString())).thenReturn(false);
FileParser fileParser = new FileParser(stubExtensionManager);
// Act
fileParser.isValidLogFileName("short_file_name.txt");
// Assert
// 當沒有指定times時,預設就是1次
Mockito.verify(stubExtensionManager).isValid(Mockito.anyString());
Mockito.verify(stubExtensionManager, Mockito.times(1)).isValid(Mockito.anyString());
}
}
可以對照上述程式,用到Mockito框架的以下方法:
- mock(Class<T> classToMock): 這邊是指定要模擬的類別或是介面。以上述程式為例,需要Mock的則是IExtensionManager。
- when(T methodCall): 這裡表示要呼叫Mock的某個方法。就如同先前手動建立Stub時,我們希望呼叫IExtensionManager的isValid方法。
- thenReturn(T value): 預期Mock呼叫某個方法後,所要回傳的值。我們希望當呼叫isValid方法之後,回傳true。
- verify(T mock) / verify(T mock, VerificationMode mode): 檢驗Mock呼叫了某個方法幾次。以上述程式碼為例,Mockito.verify(stubExtensionManager, Mockito.times(1)).isValid(Mockito.anyString()),這段程式碼表示驗證isValid方法被呼叫了1次。
以FileParser為例:@Mock的Annotation使用
- FileParserWithMockitoAnnotationTest測試程式
public class FileParserWithMockitoAnnotationTest {
@Mock
private IExtensionManager stubExtensionManager;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void testNameShorterCharactersIsValidEvenWithSupportedExtension() throws Exception {
Mockito.when(this.stubExtensionManager.isValid(Mockito.anyString())).thenReturn(true);
FileParser fileParser = new FileParser(this.stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsValidEvenWithSupportedExtension() throws Exception {
// Arrange
Mockito.when(this.stubExtensionManager.isValid(Mockito.anyString())).thenReturn(true);
FileParser fileParser = new FileParser(this.stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(true));
}
@Test
public void testNameShorterCharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
Mockito.when(this.stubExtensionManager.isValid(Mockito.anyString())).thenReturn(false);
FileParser fileParser = new FileParser(this.stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short.txt");
// Assert
assertThat(actualResult, is(false));
}
@Test
public void testNameShorterThan6CharactersIsNotValidEvenWithSupportedExtension() throws Exception {
// Arrange
Mockito.when(this.stubExtensionManager.isValid(Mockito.anyString())).thenReturn(false);
FileParser fileParser = new FileParser(this.stubExtensionManager);
// Act
boolean actualResult = fileParser.isValidLogFileName("short_file_name.txt");
// Assert
assertThat(actualResult, is(false));
}
}
Annotation的使用很簡單,只要在Mock的類別或介面加上@Mock,接著使用MockitoAnnotations.initMocks(Object testClass)方法初始化即可,同樣方式也能順利通過測試喔!如下圖。
以Bookstore為例:mock方法
- BookStoreWithMockitoTest測試程式
public class BookStoreWithMockitoTest {
@Test
public void testCheckInFeeNotVIP() throws Exception {
// Arrange
ICheckInFee mockCheckInFee = Mockito.mock(ICheckInFee.class);
BookStore bookStore = new BookStore(mockCheckInFee);
// Act
bookStore.checkInFee(this.getCustomers());
// Assert
Mockito.verify(mockCheckInFee, Mockito.times(2)).getFee(Mockito.any(Customer.class));
}
@Test
public void testCheckInFeeIsVIP() throws Exception {
// Arrange
ICheckInFee mockCheckInFee = Mockito.mock(ICheckInFee.class);
BookStore bookStore = new BookStore(mockCheckInFee);
// Act
bookStore.checkInFee(this.getCustomers());
// Assert
Mockito.verify(mockCheckInFee, Mockito.times(4)).getDiscountedFee(Mockito.any(Customer.class));
}
@Test
public void testGetIncome() throws Exception {
// Arrange
ICheckInFee mockCheckInFee = Mockito.mock(ICheckInFee.class);
Mockito.when(mockCheckInFee.getFee(Mockito.any(Customer.class))).thenReturn(1000);
Mockito.when(mockCheckInFee.getDiscountedFee(Mockito.any(Customer.class))).thenReturn(1000 * 0.8);
BookStore bookStore = new BookStore(mockCheckInFee);
// Act
bookStore.checkInFee(this.getCustomers());
// Assert
assertThat(bookStore.getIncome(), equalTo(5200.0));
}
private List<Customer> getCustomers() {
List<Customer> customers = new ArrayList<>();
Customer customer1 = new Customer();
Customer customer2 = new Customer();
Customer customer3 = new Customer();
Customer customer4 = new Customer();
Customer customer5 = new Customer();
Customer customer6 = new Customer();
customer1.setVIP(true);
customer2.setVIP(false);
customer3.setVIP(false);
customer4.setVIP(true);
customer5.setVIP(true);
customer6.setVIP(true);
customers.add(customer1);
customers.add(customer2);
customers.add(customer3);
customers.add(customer4);
customers.add(customer5);
customers.add(customer6);
return customers;
}
}
這邊的做法和FileParser的方式差不多,只是驗證結果有些差異,在Bookstore的checkInFee是void方法,我們測試則著重在ICheckInFee的getFee和getDiscountedFee方法是否被正確呼叫,而且呼叫次數是否如我們預期。以下是使用到Mockito的其他方法。
- any(Class<T> class): 這個方法表示不限定傳入特定資料,只要是同型態即可,類似的還有anyString()。
以Bookstore為例:@Mock的Annotation使用
- BookStoreWithMockitoAnnotationTest測試程式
public class BookStoreWithMockitoAnnotationTest {
@Mock
private ICheckInFee mockCheckInFee;
private BookStore bookStore;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void testCheckInFeeNotVIP() throws Exception {
// Arrange
this.bookStore = new BookStore(this.mockCheckInFee);
// Act
this.bookStore.checkInFee(this.getCustomers());
// Assert
Mockito.verify(mockCheckInFee, Mockito.times(2)).getFee(Mockito.any(Customer.class));
}
@Test
public void testCheckInFeeIsVIP() throws Exception {
// Arrange
this.bookStore = new BookStore(this.mockCheckInFee);
// Act
this.bookStore.checkInFee(this.getCustomers());
// Assert
Mockito.verify(mockCheckInFee, Mockito.times(4)).getDiscountedFee(Mockito.any(Customer.class));
}
@Test
public void testGetIncome() throws Exception {
// Arrange
Mockito.when(this.mockCheckInFee.getFee(Mockito.any(Customer.class))).thenReturn(1000);
Mockito.when(this.mockCheckInFee.getDiscountedFee(Mockito.any(Customer.class))).thenReturn(1000 * 0.8);
BookStore bookStore = new BookStore(this.mockCheckInFee);
// Act
bookStore.checkInFee(this.getCustomers());
// Assert
assertThat(bookStore.getIncome(), equalTo(5200.0));
}
private List<Customer> getCustomers() {
List<Customer> customers = new ArrayList<>();
Customer customer1 = new Customer();
Customer customer2 = new Customer();
Customer customer3 = new Customer();
Customer customer4 = new Customer();
Customer customer5 = new Customer();
Customer customer6 = new Customer();
customer1.setVIP(true);
customer2.setVIP(false);
customer3.setVIP(false);
customer4.setVIP(true);
customer5.setVIP(true);
customer6.setVIP(true);
customers.add(customer1);
customers.add(customer2);
customers.add(customer3);
customers.add(customer4);
customers.add(customer5);
customers.add(customer6);
return customers;
}
}
使用上是一樣的,所以不多做說明,大家自行練習。測試結果同樣順利,如下圖。
沒有留言:
張貼留言