Mockito

pom.xml

<!-- mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

<!-- cầu nối giữa Mockito và JUnit 5. -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>

Simple Example

  • UserDao Interface:
package com.example;

//  Data Access Object (DAO) interface for User
public interface UserDao {
    boolean createUser(String email);
}

  • UserService Interface:
package com.example;

public interface UserService {
    String createUser(String email);
}

  • UserServiceImpl class implement:
package com.example;

class UserServiceImpl implements UserService {
    private final UserDao userDao;

    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public String createUser(String email) {
        boolean created = userDao.createUser(email);
        return created ? "SUCCESS" : "FAILED";
    }
}

  • Cả 2 Interface: UserDaoUserService đều có một method là createUser tuy nhiên nó khác vai trò nhau, dù cùng "một việc" → vẫn cần tách theo vai trò

    • UserDao sẽ làm việc với database, chịu trách nhiệm insert/update với DB, chỉ chạy câu lệnh SQL.
    • UserService sẽ thực hiện xử lý logic (Có thể kiểm tra email, gửi mail, log, gọi service khác) → rồi gọi DAO → rồi xử lý kết quả.
  • Dùng interface như biến: Đây là nguyên tắc lập trình hướng interface – rất quan trọng trong Java. Viết code không phụ thuộc vào class cụ thể, giúp code dễ thay đổi, dễ test, dễ mở rộng

UserDao userDao = new MySQLUserDao(); // ✅ OK nếu MySQLUserDao implements UserDao


// ----------------------------------------------------
// trong code này UserDao sẽ được truyền từ bên ngoài vào
public class UserServiceImpl implements UserService {
    private final UserDao userDao;

    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}
// ------

// Có class nào đó implement interface UserDao
UserDao dao = new MySQLUserDao();

// Truyền vào constructor
UserService service = new UserServiceImpl(dao);

  • UserServiceImplTest:
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    void createUser_shouldReturnSuccess_whenUserCreatedSuccessfully() {
        when(userDao.createUser("test@example.com")).thenReturn(true);
        assertEquals("SUCCESS", userService.createUser("test@example.com"));
    }

    @Test
    void createUser_shouldReturnFailed_whenUserAlreadyExists() {
        when(userDao.createUser("existing@example.com")).thenReturn(false);
        assertEquals("FAILED", userService.createUser("existing@example.com"));
    }
}

  • @ExtendWith(MockitoExtension.class): Nói với JUnit 5 rằng: “Tôi muốn dùng Mockito trong file test này”

  • Tạo một object giả (mock) để thay cho class hoặc interface thật, Giúp bạn không cần implement UserDao, nhưng vẫn có thể gọi createUser():

@Mock
private UserDao userDao;
  • Tạo object thật (UserServiceImpl), tự động inject các mock như userDao vào constructor hoặc field: InjectMocks tạo userService và inject mock userDao vào constructor
@InjectMocks
private UserServiceImpl userService;

// userService = new UserServiceImpl(userDao); // 👈 userDao là mock

  • when(userDao.createUser("test@example.com")).thenReturn(true);: giả lập logic trả về true

Test Doubles in Unit Testing

  • Dummy Object:

    • Là object chỉ để truyền cho đủ tham số, không dùng đến
    • Không có annotation nào
    • Tự nhận biết nó là dummy
    • Mockito, JUnit không thể biết đó là dummy
  • Fake Object:

    • Là class tự viết, có logic thật đơn giản
    • Framework không tự nhận ra đó là "fake"
    • Được sử dụng trong môi trường test, không phù hợp cho môi trường thật.
  • Stub Object:

    • Stub là object được lập trình trả về một kết quả cố định khi gọi một method — không có logic phức tạp.
    • Stub là cách dùng, Mock là loại object → Một mock có thể dùng để stub hoặc để verify
    • when(userDao.createUser("abc@gmail.com")).thenReturn(true);: Đây là hành vi stub, nhưng object userDao vẫn là mock
  • Mock Object:

    • Là object giả dùng để kiểm tra hành vi: có được gọi đúng không, với đúng tham số không.
    • Có thể được framework nhận biết
    • Dùng @Mock hoặc Mockito.mock(...)
    • Có thể verify được hành vi: verify(...)
    • Có thể stub: when(...).thenReturn(...)
  • Spy Object:

    • Spy là bản lai giữa object thật và mock, nghĩa là nó gọi được method thật, nhưng vẫn có thể ghi đè 1 phần hành vi nếu muốn.
    • Dùng @Spy hoặc spy(obj)
    • Framework nhận biết rõ ràng
    • Cho phép gọi method thật, nhưng vẫn có thể stub hoặc verify

Basic

Enable Mockito Annotations

@ExtendWith(MockitoExtension.class)
public class MockitoAnnotationUnitTest {
    ...
}

// ---------------------

@Mock

  • @Mock tự động tạo một đối tượng giả lập của List.
@Mock
List<String> mockedList;

@Test
public void whenUseMockAnnotation_thenMockIsInjected() {
    mockedList.add("one");
    Mockito.verify(mockedList).add("one");
    assertEquals(0, mockedList.size());

    Mockito.when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

@DoNotMock

  • Được sử dụng để chỉ ra rằng một lớp hoặc giao diện không nên bị mock trong quá trình kiểm thử.
import org.mockito.exceptions.misusing.DoNotMock;

@DoNotMock(reason = "Use a real instance instead")
public abstract class NotToMock {
    // Lớp này không nên bị mock trong các kiểm thử.
}

@Spy

  • Được sử dụng để theo dõi (spy) một đối tượng thật và có thể thay đổi hành vi của các phương thức cụ thể mà không ảnh hưởng đến các phương thức khác.
@Test
public void whenNotUseSpyAnnotation_thenCorrect() {
    List<String> spyList = Mockito.spy(new ArrayList<String>());

    spyList.add("one");
    spyList.add("two");

    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");

    assertEquals(2, spyList.size());

    Mockito.doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size());
}

// ------------------------------------------------------
@Spy
List<String> spiedList = new ArrayList<String>();

@Test
public void whenUseSpyAnnotation_thenSpyIsInjectedCorrectly() {
    spiedList.add("one");
    spiedList.add("two");

    Mockito.verify(spiedList).add("one");
    Mockito.verify(spiedList).add("two");

    assertEquals(2, spiedList.size());

    Mockito.doReturn(100).when(spiedList).size();
    assertEquals(100, spiedList.size());
}

@Captor

  • Được sử dụng để tạo ra một ArgumentCaptor, giúp bắt các đối số truyền vào trong các phương thức gọi.
  • @Captor tự động tạo một ArgumentCaptor, giúp việc kiểm tra đối số của phương thức dễ dàng hơn.
@Test
public void whenNotUseCaptorAnnotation_thenCorrect() {
    List mockList = Mockito.mock(List.class);
    ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);

    mockList.add("one");
    Mockito.verify(mockList).add(arg.capture());

    assertEquals("one", arg.getValue());
}

// -------------------------------------------
@Mock
List mockedList;

@Captor
ArgumentCaptor<String> argCaptor;

@Test
public void whenUseCaptorAnnotation_thenTheSame() {
    mockedList.add("one");
    Mockito.verify(mockedList).add(argCaptor.capture());

    assertEquals("one", argCaptor.getValue());
}

  • Ví dụ:
// ---------------------capture() - getValue()
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Captor
    ArgumentCaptor<String> captor;

    @Test
    public void testCaptureAndGetValue() {
        // Gọi phương thức add() với đối số "Test"
        mockList.add("Test");

        // Bắt đối số được truyền vào phương thức add()
        verify(mockList).add(captor.capture());

        // Lấy giá trị của đối số đã được bắt
        assertEquals("Test", captor.getValue());
    }
}

// ------------------getAllValues()

package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Captor
    ArgumentCaptor<String> captor;

    @Test
    public void testGetAllValues() {
        // Gọi phương thức add() nhiều lần với các đối số khác nhau
        mockList.add("First");
        mockList.add("Second");

        // Bắt tất cả các đối số được truyền vào phương thức add()
        verify(mockList, times(2)).add(captor.capture());

        // Lấy tất cả các giá trị đã bắt được
        assertEquals(2, captor.getAllValues().size());
        assertEquals("First", captor.getAllValues().get(0));
        assertEquals("Second", captor.getAllValues().get(1));
    }
}

@InjectMocks

  • @InjectMocks được sử dụng để tự động chèn các đối tượng mock vào đối tượng được kiểm thử. Đây là một cách rất hữu ích để kiểm tra các lớp có sự phụ thuộc vào các đối tượng mock.

  • Trong khi kiểm thử, các lớp thường phụ thuộc vào các đối tượng khác (gọi là dependency) để thực hiện các hành động của chúng.

public class MyDictionary {
    private Map<String, String> wordMap;

    public MyDictionary() {
        wordMap = new HashMap<>(); // Tạo map mới trong constructor
    }

    public void add(String word, String meaning) {
        wordMap.put(word, meaning);
    }

    public String getMeaning(String word) {
        return wordMap.get(word);
    }
}
// ------------------------------------------------\
@Mock
Map<String, String> wordMap;  // Tạo mock cho Map

@InjectMocks
MyDictionary dic = new MyDictionary();  // Tạo đối tượng cần kiểm thử và tự động chèn mock vào

@Test
public void whenUseInjectMocksAnnotation_thenCorrect() {
    // Khi gọi phương thức get trên mock, mock sẽ trả về "aMeaning"
    Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");

    // Kiểm tra phương thức getMeaning trong MyDictionary
    assertEquals("aMeaning", dic.getMeaning("aWord"));
}

Assertion

  • Kiểm tra rằng kết quả thực tế của phương thức kiểm thử khớp với kết quả kỳ vọng.
package com.example;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Arrays;
import java.util.List;

public class MethodTest {

    @Test
    public void testAddition() {
        int result = 2 + 3;
        assertEquals(5, result);
    }

    @Test
    public void testPositiveCondition() {
        int result = 5 + 5;
        assertTrue(result > 0);
    }

    @Test
    public void testNotNull() {
        String str = "Hello, JUnit!";
        assertNotNull(str);
    }

    @Test
    public void testNull() {
        String str = null;
        assertNull(str);
    }

    // Phương thức test để kiểm tra ngoại lệ
    @Test
    public void testDivideByZero() {
        Calculator calculator = new Calculator();

        // Kiểm tra phương thức khi chia cho 0 có ném ra ArithmeticException không
        ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0); // Phương thức chia cho 0
        });

        // Kiểm tra thông điệp của ngoại lệ
        assertEquals("Cannot divide by zero", exception.getMessage());
    }

    // Phương thức test để kiểm tra mảng có giống nhau không
    @Test
    public void testArrayEquality() {
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};

        // Kiểm tra xem hai mảng có giống nhau không
        assertArrayEquals(expected, actual);
    }

    // Phương thức test để kiểm tra danh sách (Iterable) có giống nhau không
    @Test
    public void testIterableEquality() {
        List<String> expected = Arrays.asList("a", "b", "c");
        List<String> actual = Arrays.asList("a", "b", "c");

        assertIterableEquals(expected, actual);
    }

    // Phương thức test để kiểm tra ngoại lệ ném ra khi chia cho 0
    @Test
    public void testThrowException() {
        // Kiểm tra xem khi chia cho 0 có ném ra ngoại lệ không
        assertThrows(ArithmeticException.class, () -> {
            int result = 10 / 0; // Chia cho 0 sẽ gây ra ArithmeticException
        });
    }
}

// Lớp Calculator để kiểm thử
class Calculator {
    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Cannot divide by zero");
        }
        return a / b;
    }
}

Injecting a Mock Into a Spy

  • Mockito không hỗ trợ tự động tiêm các mock vào đối tượng spy thông qua @InjectMocks, như trong trường hợp với @Mock.
  • Cần phải tiêm (inject) các mock vào spy một cách thủ công.
import java.util.Map;

public class MyDictionary {
    private Map<String, String> wordMap;

    // Constructor để tiêm dependency
    public MyDictionary(Map<String, String> wordMap) {
        this.wordMap = wordMap;
    }

    // Phương thức thêm từ và nghĩa vào map
    public void add(String word, String meaning) {
        wordMap.put(word, meaning);
    }

    // Phương thức lấy nghĩa của từ
    public String getMeaning(String word) {
        return wordMap.get(word);
    }
}

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyDictionaryTest {

    @Mock
    Map<String, String> wordMap;  // Mock đối tượng Map

    @Spy
    MyDictionary spyDic;  // Đối tượng MyDictionary sẽ được spy

    // Trước khi chạy các test, khởi tạo các mock và spy
    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);  // Khởi tạo Mockito annotations

        // Tạo đối tượng spy của MyDictionary và tiêm mock vào thông qua constructor
        spyDic = Mockito.spy(new MyDictionary(wordMap));
    }

    @Test
    public void whenUseSpyAndMock_thenCorrect() {
        // Định nghĩa hành vi mock, khi gọi wordMap.get("aWord") sẽ trả về "aMeaning"
        Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");

        // Kiểm tra phương thức getMeaning trong MyDictionary
        String meaning = spyDic.getMeaning("aWord");

        // Kiểm tra xem kết quả có đúng không
        assertEquals("aMeaning", meaning);
    }
}


Functions

when().thenReturn()

Mockito.any()

  • Khớp bất kỳ đối số nào
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testAnyMethod() {
        // Stubbing phương thức add với bất kỳ đối số nào
        when(mockList.add(any(String.class))).thenReturn(true);

        // Kiểm tra
        assertTrue(mockList.add("Test"));
        assertTrue(mockList.add("Another Test"));
    }
}


Mockito.doReturn().when()

  • Dùng cho spy để tránh lỗi NullPointerException

  • NullPointerException: là một loại lỗi trong Java xảy ra khi cố gắng thực hiện một thao tác trên một đối tượng có giá trị là null.

  • Ví dụ như với when(...).thenReturn(...) với Spy: nó sẽ gọi phương thức thật của đối tượng spy trước khi áp dụng hành vi giả lập (stubbing). Điều này có thể gây ra lỗi NullPointerException nếu phương thức thật đang stub có thao tác với các đối tượng hoặc dữ liệu có giá trị null hoặc chưa được khởi tạo đầy đủ.

  • Do đó sử dụng doReturn().when() với spy thay vì when(...).thenReturn(...). Lý do là doReturn().when() sẽ không gọi phương thức thực mà chỉ thay đổi hành vi của phương thức mà không thực thi bất kỳ logic nào bên trong phương thức đó.

import org.junit.jupiter.api.Test;
import org.mockito.Spy;
import static org.mockito.Mockito.*;

public class SpyTest {

    @Spy
    OrderService orderService = new OrderService();

    @Test
    public void testCalculateTotal() {
        // Khi gọi calculateTotal(), spy sẽ gọi phương thức thật
        when(orderService.calculateTotal()).thenReturn(10);  // Sẽ gây NPE nếu không khởi tạo 'items'

        // Lỗi: NullPointerException vì items chưa được khởi tạo
        System.out.println(orderService.calculateTotal());
    }
}

// -------------------------------------------------------------------
import org.junit.jupiter.api.Test;
import org.mockito.Spy;
import static org.mockito.Mockito.*;

public class SpyTest {

    @Spy
    OrderService orderService = new OrderService();

    @Test
    public void testCalculateTotal() {
        // Đảm bảo không gọi phương thức thật, tránh NPE
        doReturn(10).when(orderService).calculateTotal();

        // Kiểm tra phương thức đã trả về giá trị giả lập
        System.out.println(orderService.calculateTotal());  // Output: 10
    }
}

Mockito.times()

  • Kiểm tra số lần gọi một phương thức
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import java.util.List;

@ExtendWith(MockitoExtension.class)
public class TimesMethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testTimes() {
        mockList.add("one");
        mockList.add("two");

        // Kiểm tra phương thức add() đã được gọi 2 lần
        verify(mockList, times(2)).add(anyString());
    }
}

Mockito.atLeast()

  • Kiểm tra phương thức được gọi ít nhất một số lần
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import java.util.List;

@ExtendWith(MockitoExtension.class)
public class AtLeastMethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testAtLeast() {
        mockList.add("one");
        mockList.add("two");

        // Kiểm tra phương thức add() đã được gọi ít nhất 1 lần
        verify(mockList, atLeast(1)).add(anyString());
    }
}

Mockito.atMost()

  • Kiểm tra phương thức không được gọi quá một số lần
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

import java.util.List;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testAtMost() {
        mockList.add("one");
        mockList.add("two");
        mockList.add("three");

        // Kiểm tra phương thức add() không được gọi quá 3 lần
        verify(mockList, atMost(3)).add(anyString());
    }
}

Mockito.never()

// Kiểm tra phương thức không được gọi
verify(mockList, never()).add(anyString());

Mockito.timeout()

  • Kiểm tra số lần gọi phương thức trong một khoảng thời gian nhất định
verify(mockList, timeout(100).times(1)).add(anyString());

Mockito.reset()

  • Reset lại mock để sử dụng lại trong các kiểm thử khác
public void testReset() {
    mockList.add("one");

    // Reset lại mock, không còn tương tác nào
    reset(mockList);

    // Sau khi reset, mockList không giữ lại trạng thái cũ
    verify(mockList, never()).add("one");
}

Mockito.verifyNoMoreInteractions()

  • Kiểm tra không có tương tác nào khác ngoài các phương thức đã xác nhận
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

import java.util.List;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testNoMoreInteractions() {
        mockList.add("one");
        mockList.add("two");

        // Kiểm tra rằng không có tương tác nào khác ngoài các phương thức add
        verify(mockList).add("one");
        verify(mockList).add("two");

        verifyNoMoreInteractions(mockList);
    }
}

Mockito.doThrow()

  • Định nghĩa hành vi ném ngoại lệ khi phương thức của mock được gọi
  • Mô phỏng hành vi ném ngoại lệ (exception) khi gọi phương thức trên đối tượng mock bằng cách sử dụng doThrow() trong Mockito. Cụ thể, trong ví dụ này, phương thức remove() của List sẽ ném ra một ngoại lệ UnsupportedOperationException khi được gọi.
package com.example;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;

@ExtendWith(MockitoExtension.class)
public class MethodTest {

    @Mock
    List<String> mockList;

    @Test
    public void testDoThrow() {
        // Định nghĩa rằng khi gọi phương thức remove() trên mockList sẽ ném ngoại lệ
        doThrow(new UnsupportedOperationException("Unsupported operation")).when(mockList).remove(anyInt());

        // Kiểm tra ngoại lệ
        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, () -> {
            mockList.remove(0);
        });

        assertEquals("Unsupported operation", thrown.getMessage());
    }
}