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: