Trong bài viết hôm nay, chúng ta sẽ học cách mock dependency injection object trong unit test bằng @InjectMocks – một trong những annotation mạnh mẽ và tiện lợi nhất của Mockito.

@InjectMocks giúp chúng ta “tiêm” (inject) các dependency đã được mock vào đối tượng của lớp cần test một cách tự động như thế nào, qua đó đơn giản hóa đáng kể quá trình thiết lập test và giảm thiểu boilerplate code. Chúng ta cũng sẽ tìm hiểu về các cơ chế injection khác nhau mà Mockito sử dụng và cách áp dụng chúng trong thực tế.
Tiêm Dependency tự động như thế nào?
Annotation @InjectMocks của Mockito cho phép chúng ta tự động tiêm (inject) các dependency đã được mock (hoặc spy) vào một đối tượng của lớp được đánh dấu bằng chính @InjectMocks. Điều này cực kỳ hữu ích khi lớp mà chúng ta muốn kiểm thử (System Under Test – SUT) có các dependency bên ngoài cần được cô lập hoặc kiểm soát hành vi trong quá trình test. Các đối tượng mock được tiêm vào sẽ được khai báo bằng @Mock hoặc @Spy.
Các cơ chế Injection của Mockito
Mockito sẽ cố gắng tiêm các dependency đã được mock theo ba cách tiếp cận, với một thứ tự ưu tiên nhất định:
- Constructor Based Injection (Tiêm qua Constructor): Đây là phương pháp ưu tiên hàng đầu. Khi có một constructor được định nghĩa cho lớp, Mockito sẽ cố gắng tiêm các dependency bằng cách sử dụng constructor có số lượng tham số lớn nhất (hoặc constructor phù hợp nhất với các mock có sẵn). Nếu có nhiều constructor với cùng số lượng tham số, Mockito có thể gặp khó khăn hoặc cần tên mock để phân biệt.
- Setter Methods Based Injection (Tiêm qua Setter): Nếu không có constructor nào phù hợp hoặc không có constructor nào được định nghĩa, Mockito sẽ chuyển sang cố gắng tiêm các dependency thông qua các phương thức setter (ví dụ:
setEmailService(EmailService emailService)). - Field Based Injection (Tiêm trực tiếp vào Trường): Cuối cùng, nếu không có constructor hay phương thức setter nào khả dụng để tiêm dependency, Mockito sẽ cố gắng tiêm trực tiếp vào các trường (fields) của lớp.
Quy tắc tìm mock:
- Nếu chỉ có một đối tượng mock khớp với kiểu dữ liệu của dependency, Mockito sẽ tiêm đối tượng đó vào.
- Nếu có nhiều hơn một đối tượng mock cùng kiểu dữ liệu, Mockito sẽ sử dụng tên của đối tượng mock để khớp và tiêm dependency. Ví dụ, nếu một lớp có
private EmailService primaryEmailService;và bạn có@Mock EmailService primaryEmailService;, Mockito sẽ cố gắng khớp theo tên.
Ví dụ về @InjectMocks
Để thấy rõ cách Mockito thực hiện việc tiêm dependency của các mock, chúng ta hãy tạo một số service và các lớp ứng dụng có dependency.
Các lớp Service
Đầu tiên là một interface Service và hai lớp triển khai đơn giản:
package com.journaldev.injectmocksservices;
public interface Service {
public boolean send(String msg);
}
package com.journaldev.injectmocksservices;
public class EmailService implements Service {
@Override
public boolean send(String msg) {
System.out.println("Sending email");
return true;
}
}
package com.journaldev.injectmocksservices;
public class SMSService implements Service {
@Override
public boolean send(String msg) {
System.out.println("Sending SMS");
return true;
}
}
Tạo lớp App Service với Dependencies
Tiếp theo, chúng ta sẽ tạo ba lớp AppServices khác nhau, mỗi lớp đại diện cho một cách Mockito có thể tiêm dependency:
AppServices: Sử dụng Constructor Based Injection.
package com.journaldev.injectmocksservices;
// Dành cho Constructor Based @InjectMocks injection
public class AppServices {
private EmailService emailService;
private SMSService smsService;
public AppServices(EmailService emailService, SMSService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
public boolean sendSMS(String msg) {
return smsService.send(msg);
}
public boolean sendEmail(String msg) {
return emailService.send(msg);
}
}
AppServices1: Sử dụng Property Setter Based Injection.
package com.journaldev.injectmocksservices;
// Dành cho Property Setter Based @InjectMocks injection
public class AppServices1 {
private EmailService emailService;
private SMSService smsService;
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
public void setSmsService(SMSService smsService) {
this.smsService = smsService;
}
public boolean sendSMS(String msg) {
return smsService.send(msg);
}
public boolean sendEmail(String msg) {
return emailService.send(msg);
}
}
AppServices2: Sử dụng Field Based Injection.
package com.journaldev.injectmocksservices;
// Dành cho Field Based @InjectMocks injection
public class AppServices2 {
private EmailService emailService;
private SMSService smsService;
public boolean sendSMS(String msg) {
return smsService.send(msg);
}
public boolean sendEmail(String msg) {
return emailService.send(msg);
}
}
Các ví dụ test với @InjectMocks
Giờ là lúc chúng ta viết các test case để xem @InjectMocks hoạt động như thế nào với từng kiểu injection. Lưu ý rằng lớp test của tôi là MockitoInjectMocksExamples đang extend BaseTestCase. Điều này nhằm mục đích khởi tạo các mock của Mockito trước khi các test chạy. Đây là mã của lớp BaseTestCase:
package com.journaldev.mockito.injectmocks;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.MockitoAnnotations;
class BaseTestCase {
@BeforeEach
void init_mocks() {
MockitoAnnotations.initMocks(this);
}
}
Rất quan trọng: Nếu bạn không gọi MockitoAnnotations.initMocks(this); (hoặc sử dụng @RunWith(MockitoJUnitRunner.class) nếu dùng JUnit 4, hoặc @ExtendWith(MockitoExtension.class) nếu dùng JUnit 5), bạn sẽ gặp lỗi NullPointerException vì các đối tượng mock sẽ không được khởi tạo.
Và đây là lớp test chính của chúng ta:
MockitoInjectMocksExamples
package com.journaldev.mockito.injectmocks;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import com.journaldev.injectmocksservices.AppServices;
import com.journaldev.injectmocksservices.AppServices1;
import com.journaldev.injectmocksservices.AppServices2;
import com.journaldev.injectmocksservices.EmailService;
import com.journaldev.injectmocksservices.SMSService;
class MockitoInjectMocksExamples extends BaseTestCase {
@Mock EmailService emailService;
@Mock SMSService smsService;
@InjectMocks AppServices appServicesConstructorInjectionMock;
@InjectMocks AppServices1 appServicesSetterInjectionMock;
@InjectMocks AppServices2 appServicesFieldInjectionMock;
@Test
void test_constructor_injection_mock() {
// Thiết lập hành vi cho emailService và smsService đã được inject vào appServicesConstructorInjectionMock
when(appServicesConstructorInjectionMock.sendEmail("Email")).thenReturn(true);
when(appServicesConstructorInjectionMock.sendSMS(anyString())).thenReturn(true);
// Kiểm tra các trường hợp đã được stub
assertTrue(appServicesConstructorInjectionMock.sendEmail("Email"));
// Kiểm tra trường hợp chưa được stub, mặc định Mockito trả về false cho boolean
assertFalse(appServicesConstructorInjectionMock.sendEmail("Unstubbed Email"));
assertTrue(appServicesConstructorInjectionMock.sendSMS("SMS"));
}
}
@InjectMocksSetter Methods Injection Example
Tiếp theo là test case cho phương thức tiêm qua setter. Test này nằm trong cùng lớp MockitoInjectMocksExamples.
@Test
void test_setter_injection_mock() {
// Thiết lập hành vi cho emailService và smsService đã được inject vào appServicesSetterInjectionMock
when(appServicesSetterInjectionMock.sendEmail("New Email")).thenReturn(true);
when(appServicesSetterInjectionMock.sendSMS(anyString())).thenReturn(true);
// Kiểm tra các trường hợp đã được stub
assertTrue(appServicesSetterInjectionMock.sendEmail("New Email"));
assertFalse(appServicesSetterInjectionMock.sendEmail("Unstubbed Email"));
assertTrue(appServicesSetterInjectionMock.sendSMS("SMS"));
}
@InjectMocksField Based Injection Example
Và cuối cùng là test case cho phương thức tiêm trực tiếp vào trường (field). Test này cũng nằm trong cùng lớp MockitoInjectMocksExamples.
@Test
void test_field_injection_mock() {
// Thiết lập hành vi cho emailService và smsService đã được inject vào appServicesFieldInjectionMock
when(appServicesFieldInjectionMock.sendEmail(anyString())).thenReturn(true);
when(appServicesFieldInjectionMock.sendSMS(anyString())).thenReturn(true);
// Với anyString(), mọi chuỗi đều được trả về true
assertTrue(appServicesFieldInjectionMock.sendEmail("Email"));
assertTrue(appServicesFieldInjectionMock.sendEmail("New Email"));
assertTrue(appServicesFieldInjectionMock.sendSMS("SMS"));
}
Bạn có thể thấy, dù ba lớp AppServices có cấu trúc khác nhau để nhận dependency, Mockito với @InjectMocks vẫn “hiểu” và tiêm các mock tương ứng vào một cách mượt mà. Điều này giúp chúng ta tập trung vào việc định nghĩa hành vi của các mock mà không phải lo lắng về việc khởi tạo và nối kết chúng với lớp cần test.
Kết Luận
Qua bài viết này, chúng ta đã cùng nhau khám phá sâu hơn về annotation @InjectMocks của Mockito – một công cụ vô cùng mạnh mẽ giúp đơn giản hóa quá trình viết unit test trong Java. Chúng ta đã hiểu rõ cách @InjectMocks tự động tiêm các dependency đã được mock thông qua ba cơ chế ưu tiên: Constructor, Setter, và Field.
Hãy áp dụng kiến thức này vào các dự án thực tế của bạn để xây dựng các bộ unit test mạnh mẽ, hiệu quả hơn. Việc làm chủ Mockito và các kỹ thuật mocking sẽ nâng cao đáng kể chất lượng code và quy trình phát triển phần mềm của bạn.