Toàn bộ ý tưởng của việc tạo một mock object là có thể kiểm soát behavior của nó. Nếu một phương thức của mock được gọi, nó sẽ xử lý theo cách mà chúng ta có thể điều khiển được. Trong bài viết này, chúng ta sẽ cùng tìm hiểu các cách để điều khiển behavior của một đối tượng giả (mock object).
Stubbing Methods – when().thenXxx()
Chúng ta muốn điều khiển một mock object và xác định phải làm gì khi các phương thức cụ thể của mock object được gọi. Điều này được gọi là Stubbing.
Mockito.when(T methodCall): dùng để giả lập một lời gọi hàm nào đó được sử dụng bên trong method đang được kiểm thử.
Phương thức Mockito.when() thường đi kèm với thenReturn(), thenAnswer(), … để chỉ định kết quả trả về. Ý nghĩa của cấu trúc này đơn giản là: “When the x method is called then return y” – “Khi một phương thức x được gọi thì return về một giá trị y”.
- thenReturn() : chỉ định trả về một giá trị cụ thể.
- thenThrow() : chỉ định trả về một Exception.
- thenAnswer(): thực hiện xử lý các lệnh định nghĩa bên trong phương thức answer().
- thenCallRealMethod(): chỉ định phương thức thực sự được gọi. Khi sử dụng phương thức này chúng ta nên chắc chắn rằng nó an toàn, bởi vì implement thực sự của phương thức nếu throw một exception hay phụ thuộc vào trạng thái cụ thể của object có thể xảy ra lỗi, không thể thực thi được.
Ví dụ Stubbing Methods với when().thenXxx()
CustomList.java
package com.gpcoder.mockito.whenthen;
import java.util.ArrayList;
import java.util.List;
public class CustomList {
private List<String> list;
public boolean add(String item) {
getList().add(item);
return true;
}
public String get(int index) {
return getList().get(index);
}
public int size() {
return getList().size();
}
public int clear() {
getList().clear();
return getList().size();
}
private List<String> getList() {
if (list == null) {
list = new ArrayList<>();
}
return list;
}
}
WhenThenTest.java
package com.gpcoder.mockito.whenthen;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class WhenThenTest {
private CustomList mockedObject;
@Before
public void prepareForTest() {
// Mock creation
mockedObject = Mockito.mock(CustomList.class);
}
@Test
public void thenReturnTest() {
// Configure mock to return a specific value on a method call
Mockito.when(mockedObject.get(0)).thenReturn("gpcoder.com");
// Verify behavior
Assert.assertEquals("gpcoder.com", mockedObject.get(0));
}
@Test(expected = IllegalStateException.class)
public void thenThrowTest() {
// Configure mock to throw an exception on a method call
Mockito.when(mockedObject.add(Mockito.anyString())).thenThrow(IllegalStateException.class);
mockedObject.add("gpcoder.com");
}
@Test
public void thenAnswerTest1() {
// Configure mock method call with custom Answer
Mockito.when(mockedObject.get(Mockito.anyInt())).thenAnswer(new Answer<String>() {
public String answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
// Object mockedObject = invocation.getMock();
return "gpcoder.com" + Arrays.toString(args);
}
});
// Verify behavior
Assert.assertEquals("gpcoder.com[1]", mockedObject.get(1));
}
@Test
public void thenAnswerTest2() {
// Configure mock method call with custom Answer using Java 8 syntax
Mockito.when(mockedObject.get(Mockito.anyInt())).thenAnswer(invocation -> {
Object[] args = invocation.getArguments();
// Object mockedObject = invocation.getMock();
return "gpcoder.com" + Arrays.toString(args);
});
// Verify behavior
Assert.assertEquals("gpcoder.com[1]", mockedObject.get(1));
}
@Test
public void thenCallRealMethodTest() {
// Configure mock method call real method
// Be sure the real implementation is 'safe'.
// If real implementation throws exceptions or depends on specific state of the
// object then you're in trouble.
Mockito.when(mockedObject.add(Mockito.anyString())).thenCallRealMethod();
Mockito.when(mockedObject.get(Mockito.anyInt())).thenCallRealMethod();
Mockito.when(mockedObject.size()).thenCallRealMethod();
mockedObject.add("gpcoder.com");
mockedObject.clear(); // This method will be not called on mocked object
// Verify behavior
Assert.assertEquals(1, mockedObject.size());
Assert.assertEquals("gpcoder.com", mockedObject.get(0));
}
}
Lưu ý: Với @Spy object, mặc định các phương thức sẽ được gọi thực sự, khi cần chỉ định kết quả trả về chúng ta sẻ sử dụng cấu trúc when().thenXxx(). Ngược lại với Spy, với @Mock object, mặc định các phương thức sẽ không được gọi, khi cần thực thi phương thức thực sự, chúng ta sẽ sử dụng cấu trúc when().thenCallRealMethod().
Ví dụ gọi nhiều thenReturn() liên tiếp nhau – Stubbing consecutive calls (iterator-style stubbing)
package com.gpcoder.mockito.whenthen;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class IteratorThenReturnTest {
private CustomList mockedObject;
@Before
public void prepareForTest() {
// Mock creation
mockedObject = Mockito.mock(CustomList.class);
}
@Test
public void consecutiveStubbingTest1() {
// Configure mock to return a specific value on a method call
Mockito.when(mockedObject.get(0)).thenReturn("one").thenReturn("two").thenReturn("three");
// Verify behavior
Assert.assertEquals("one", mockedObject.get(0));
Assert.assertEquals("two", mockedObject.get(0));
// From 3rd times, it always return "three" value
Assert.assertEquals("three", mockedObject.get(0));
Assert.assertEquals("three", mockedObject.get(0));
Assert.assertEquals("three", mockedObject.get(0));
}
@Test
public void consecutiveStubbingTest2() {
// Configure mock to return a specific value on a method call
Mockito.when(mockedObject.get(0)).thenReturn("one", "two", "three");
// Verify behavior
Assert.assertEquals("one", mockedObject.get(0));
Assert.assertEquals("two", mockedObject.get(0));
// From 3rd times, it always return "three" value
Assert.assertEquals("three", mockedObject.get(0));
Assert.assertEquals("three", mockedObject.get(0));
Assert.assertEquals("three", mockedObject.get(0));
}
@Test
public void overrideStubbingTest() {
// Configure mock to return a specific value on a method call
Mockito.when(mockedObject.get(0)).thenReturn("first");
Mockito.when(mockedObject.get(0)).thenReturn("second");
// Verify behavior
// All mockedObject.get(0) calls will return "second"
Assert.assertEquals("second", mockedObject.get(0));
Assert.assertEquals("second", mockedObject.get(0));
}
}
Ví dụ deep stubs method cho legacy code
Đôi khi trong hệ thống cũ, có những đoạn legacy code chúng ta cần phải viết test cho nó. Những đoạn code này đang vi phạm định luật Law of Demeter, ví dụ:
store.getOrder().getCustomer().getBillingAddress().getCity();
Để test được đoạn code trên chúng ta cần tạo mock object cho Store, sau đó sử dụng cấu trúc when(store.getOrder()).thenReturn(order) để trả về một mock object cho Order, tiếp tục when(order.getCustomer()).thenReturn(billingAddress) để trả về một mock object cho BillingAddress, cứ tiếp tục cho đến khi chỉ định được kết quả xử lý cho getCity().
Thật may mắn, Mockito hỗ trợ cho chúng ta một cách để có thể deep stubs và chỉ định kết quả mong muốn ở last mock chain thông qua phương thức tạo Mock Object:
mock(classToMock, RETURNS_DEEP_STUBS)
Ví dụ:
Store.java
package com.gpcoder.mockito.whenthen;
import lombok.Data;
@Data
class BillingAddress {
private String city;
}
@Data
class Customer {
private BillingAddress billingAddress;
}
@Data
class Order {
private Customer customer;
}
@Data
public class Store {
private Order order;
}
DeepStubTest.java
package com.gpcoder.mockito.whenthen;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
public class DeepStubTest {
@Test
public void withoutDeepStubTest() {
// Create mock object
Store store = Mockito.mock(Store.class);
Order order = Mockito.mock(Order.class);
Customer customer = Mockito.mock(Customer.class);
BillingAddress billingAddress = Mockito.mock(BillingAddress.class);
// Configure mock to return a specific value on a method call
Mockito.when(store.getOrder()).thenReturn(order);
Mockito.when(order.getCustomer()).thenReturn(customer);
Mockito.when(customer.getBillingAddress()).thenReturn(billingAddress);
Mockito.when(billingAddress.getCity()).thenReturn("Can Tho");
// Verify behavior
Assert.assertEquals("Can Tho", store.getOrder().getCustomer().getBillingAddress().getCity());
}
@Test
public void deepStubTest() {
// Create mock object
Store store = Mockito.mock(Store.class, Mockito.RETURNS_DEEP_STUBS);
// Configure mock to return a specific value on a method call
Mockito.when(store.getOrder().getCustomer().getBillingAddress().getCity()).thenReturn("Can Tho");
// Verify behavior
Assert.assertEquals("Can Tho", store.getOrder().getCustomer().getBillingAddress().getCity());
}
}
Stubbing Methods – doXxx().when()
Cấu trúc doXxx().when() được sử dụng khi:
- Chỉ định cách xử lý một phương thức void().
- Chỉ định kết quả xử lý trên các phương thức của @Spy object.
- Chỉ định các kết quả test khác nhau (nhiều hơn 1 lần trong khi thực thi 1 phương thức test).
Cấu trúc doXxx().when() tương tự như when().thenXxx(), nó cho cùng một kết quả. Chúng ta nên cố gắng sử dụng cấu trúc when().thenXxx() trong hầu hầu hết trường hợp, ngoại trừ những trường hợp đã liệt kê ở trên, bởi vì các argument của cấu trúc cấu trúc when().thenXxx() là type-safe và dễ đọc hơn.
- doReturn() : chỉ định trả về một giá trị cụ thể.
- doThrow() : chỉ định trả về một Exception.
- doAnswer() : thực hiện xử lý các lệnh định nghĩa bên trong phương thức answer().
- doNothing() : chỉ định phương thức này không làm gì.
- doCallRealMethod() : chỉ định phương thức thực sự được gọi. Khi sử dụng phương thức này chúng ta nên chắc chắn rằng nó an toàn, bởi vì implement thực sự của phương thức nếu throw một exception hay phụ thuộc vào trạng thái cụ thể của object có thể xảy ra lỗi, không thể thực thi được.
Ví dụ:
package com.gpcoder.mockito.dothen;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import com.gpcoder.mockito.whenthen.CustomList;
public class DoWhenTest {
private CustomList mockedObject;
@Before
public void prepareForTest() {
// Mock creation
mockedObject = Mockito.mock(CustomList.class);
}
@Test
public void doReturnTest() {
// Configure mock to return a specific value on a method call
Mockito.doReturn("gpcoder.com").when(mockedObject).get(0);
// Verify behavior
Assert.assertEquals("gpcoder.com", mockedObject.get(0));
}
@Test(expected = IllegalStateException.class)
public void doThrowTest() {
// Configure mock to throw an exception on a method call
Mockito.doThrow(IllegalStateException.class).when(mockedObject).add(Mockito.anyString());
mockedObject.add("gpcoder.com");
}
@Test
public void doAnswerTest1() {
// Configure mock method call with custom Answer
Mockito.doAnswer(new Answer<String>() {
public String answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
// Object mockedObject = invocation.getMock();
return "gpcoder.com" + Arrays.toString(args);
}
}).when(mockedObject).get(Mockito.anyInt());
// Verify behavior
Assert.assertEquals("gpcoder.com[1]", mockedObject.get(1));
}
@Test
public void doAnswerTest2() {
// Configure mock method call with custom Answer using Java 8 syntax
Mockito.doAnswer(invocation -> {
Object[] args = invocation.getArguments();
// Object mockedObject = invocation.getMock();
return "gpcoder.com" + Arrays.toString(args);
}).when(mockedObject).get(Mockito.anyInt());
// Verify behavior
Assert.assertEquals("gpcoder.com[1]", mockedObject.get(1));
}
@Test
public void doCallRealMethodTest() {
// Configure mock method call real method
// Be sure the real implementation is 'safe'.
// If real implementation throws exceptions or depends on specific state of the
// object then you're in trouble.
Mockito.doCallRealMethod().when(mockedObject).add(Mockito.anyString());
Mockito.doCallRealMethod().when(mockedObject).get(Mockito.anyInt());
Mockito.doCallRealMethod().when(mockedObject).size();
mockedObject.add("gpcoder.com");
mockedObject.clear(); // This method will be not called on mocked object
// Verify behavior
Assert.assertEquals(1, mockedObject.size());
Assert.assertEquals("gpcoder.com", mockedObject.get(0));
}
@Test
public void doNothingTest() {
// Configure mock method call with custom Answer using Java 8 syntax
Mockito.doNothing().when(mockedObject).remove(0);
mockedObject.remove(0);
// Verify behavior
Mockito.verify(mockedObject, Mockito.times(1)).remove(0);
}
}
Argument matchers
Các method Mockito.anyString(), Mockito.anyInt(), Mockito.any(),… thường được dùng khi mock các phương thức có tham số, khi mà chúng ta không xác định được giá trị của các tham số đó.
Ví dụ sử dụng Argurment Matcher
public class ArgumentMatcherTest {
@Test
public void anyIntTest() {
List<String> mockedList = Mockito.mock(List.class);
// Configure mock to return a specific value on a method call
Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("gpcoder.com");
Mockito.when(mockedList.add(Mockito.anyString())).thenReturn(true);
// Verify behavior
Assert.assertEquals(true, mockedList.add("gpcoder.com"));
Assert.assertEquals(true, mockedList.add("mockito"));
Assert.assertEquals("gpcoder.com", mockedList.get(0));
Assert.assertEquals("gpcoder.com", mockedList.get(4));
}
}
Ví dụ Custom Argument Matcher
@Data
@AllArgsConstructor
class Message {
private String to;
private String content;
}
class MessageMatcher implements ArgumentMatcher<Message> {
private Message message;
public MessageMatcher(Message message) {
this.message = message;
}
// Informs if this matcher accepts the given argument. The method should never
// assert if the argument doesn't match. Itshould only return false.
@Override
public boolean matches(Message message) {
if (this.message.equals(message)) {
return true;
}
return false;
}
}
public class ArgumentMatcherTest {
@Test
public void customArgumentMatcherTest() {
List<Message> mockedList = Mockito.mock(List.class);
Message message = new Message("gpcoder.com", "Custom Argument Matcher");
// Configure mock to return a specific value on a method call
Mockito.when(mockedList.add(Mockito.argThat(new MessageMatcher(message)))).thenReturn(true);
// Verify behavior
Assert.assertEquals(true, mockedList.add(message));
}
}
So sánh Custom Argument Matcher vs ArgumentCaptor:
- ArgumentCaptor : được sử dụng để verify các giá trị của argument.
- Custom Argument Matcher : được sử dụng cho các argument stub.
Tài liệu tham khảo: