[Investigation] Test double? Mock? Stub? Spy? Fake?
Lời mở đầu
Một vấn đề mà khá nhiều lập trình viên mắc phải đó là sử dụng những thứ mà mình chưa hiểu rõ về nó. Mình cũng vậy 😅😂😂Dù đã viết khá nhiều UT cho các dự án khác nhau nhưng đến bây giờ mình vẫn cảm thấy khá là nhập nhằng giữa các khác niệm được nhắc đến ở tiêu đề bài viết này. Vậy là mình ngồi đây, tìm hiểu và cùng xem lại các khái niệm về UT này là gì và có những sự khác biệt gì giữa chúng. Ok. Let's go!!!
Khái niệm
Chắc các bạn đã từng nghe trên một lần các khái niệm như test double, mock, stub, spy, fake trong một buổi sharing nào đấy. Hoặc thoáng qua trong một bài viết về chuyên môn. Nhưng tại sao chúng ta lại cần sử dụng chúng? Đầu tiên ta nhắc lại kiến thức cũ một chút. Unit test là gì? Unit test là một phương pháp kiểm thử phần mềm trong đó các đoạn mã (code) được kiểm tra độc lập và cụ thể, từng phần riêng lẻ của chương trình (như hàm, phương thức, lớp) được kiểm thử để đảm bảo chức năng của nó hoạt động đúng như mong đợi.
Vậy nên Test double được sinh ra để sử dụng trong kiểm thử phần mềm để chỉ các đối tượng giả định được sử dụng để thay thế cho các thành phần phụ thuộc của một đơn vị thử nghiệm. Test double được sử dụng để cô lập đơn vị thử nghiệm khỏi các thành phần phụ thuộc mà không cần thực sự sử dụng các thành phần đó.
Có nhiều loại test double, trong đó "Test double" là thuật ngữ tổng quát để chỉ đến các loại sau: Mock, Stub, Spy, Fake.
Mock
Là một đối tượng giả định được sử dụng để kiểm tra xem một phương thức đã được gọi hay chưa, và nếu được gọi, phương thức đó được gọi với các tham số chính xác hay không.
public class Calculator {
private final AdditionService additionService;
public Calculator(AdditionService additionService) {
this.additionService = additionService;
}
public int add(int x, int y) {
return additionService.add(x, y);
}
}
public interface AdditionService {
int add(int x, int y);
}
public class AdditionServiceImpl implements AdditionService {
public int add(int x, int y) {
return x + y;
}
}
public class CalculatorTest {
private AdditionService mockAdditionService;
private Calculator calculator;
@Before
public void setUp() {
mockAdditionService = mock(AdditionService.class);
calculator = new Calculator(mockAdditionService);
}
@Test
public void testAddition() {
// Giả định AdditionService trả về 5 khi được gọi với các tham số (2, 3)
when(mockAdditionService.add(2, 3)).thenReturn(5);
// Kiểm tra kết quả trả về của phương thức add trong lớp Calculator
assertEquals(5, calculator.add(2, 3));
// Kiểm tra xem phương thức add của AdditionService đã được gọi với đúng các tham số hay chưa
verify(mockAdditionService).add(2, 3);
}
}
Trong ví dụ này, chúng ta đang kiểm thử lớp Calculator bằng cách sử dụng một test double cho AdditionService, được thực hiện bằng cách sử dụng Mockito để tạo ra một đối tượng mock cho AdditionService. Chúng ta giả định rằng AdditionService sẽ trả về giá trị 5 khi được gọi với các tham số là 2 và 3, sau đó kiểm tra kết quả trả về của phương thức add trong lớp Calculator. Chúng ta cũng kiểm tra xem phương thức add của AdditionService đã được gọi với đúng các tham số hay chưa bằng cách sử dụng phương thức verify của Mockito.
Stub
Là một đối tượng giả định được sử dụng để trả về giá trị cứng định trước cho một cuộc gọi phương thức cụ thể.
public class Order {
private final PaymentGateway paymentGateway;
public Order(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void checkout() {
// Thực hiện việc thanh toán bằng cách sử dụng PaymentGateway
boolean success = paymentGateway.pay(100.0);
if (success) {
System.out.println("Order successful");
} else {
System.out.println("Order failed");
}
}
}
public interface PaymentGateway {
boolean pay(double amount);
}
public class PaymentGatewayStub implements PaymentGateway {
@Override
public boolean pay(double amount) {
// Luôn trả về giá trị true
return true;
}
}
public class OrderTest {
@Test
public void testCheckout() {
PaymentGatewayStub paymentGatewayStub = new PaymentGatewayStub();
Order order = new Order(paymentGatewayStub);
order.checkout();
// Kiểm tra xem Order đã in ra đúng thông báo khi thanh toán thành công hay không
assertEquals("Order successful", systemOutRule.getLog().trim());
}
}
Trong ví dụ này, chúng ta đã sử dụng PaymentGatewayStub để cung cấp giá trị mặc định cho phương thức pay của PaymentGateway, để kiểm tra xem lớp Order đã in ra đúng thông báo khi thanh toán thành công hay không.
Spy
Là một đối tượng giả định được sử dụng để ghi lại thông tin về các cuộc gọi phương thức cụ thể, ví dụ như số lần gọi, các tham số được truyền vào và giá trị trả về.
public class Calculator {
public int add(int x, int y) {
return x + y;
}
}
public class CalculatorSpy extends Calculator {
private int addCount = 0;
@Override
public int add(int x, int y) {
addCount++;
return super.add(x, y);
}
public int getAddCount() {
return addCount;
}
}
public class CalculatorTest {
@Test
public void testAdd() {
CalculatorSpy calculatorSpy = new CalculatorSpy();
int result = calculatorSpy.add(2, 3);
assertEquals(5, result);
assertEquals(1, calculatorSpy.getAddCount());
}
}
Trong ví dụ này, chúng ta đã tạo ra một CalculatorSpy để giám sát việc gọi phương thức add của Calculator. Chúng ta đã ghi lại số lần phương thức này được gọi bằng cách sử dụng một biến đếm và kiểm tra giá trị trả về của phương thức add, cũng như số lần phương thức được gọi đến.
Fake
Các phương thức giả lập như stub, mock và spy đôi khi không đủ để kiểm tra các trường hợp phức tạp hoặc yêu cầu sử dụng một đối tượng thực sự để thực hiện các hành động phức tạp. Trong trường hợp này, bạn có thể sử dụng một đối tượng giả định là một đối tượng giả (fake object).
Là một đối tượng giả định được sử dụng để cung cấp một triển khai thay thế cho một thành phần phụ thuộc thực sự, giúp giảm thiểu các phụ thuộc đến các thành phần bên ngoài hoặc giảm thiểu sự phức tạp của việc thiết kế và triển khai.
public interface UserDao {
User getUser(int userId);
}
public class UserDaoImpl implements UserDao {
@Override
public User getUser(int userId) {
// Code để lấy User từ cơ sở dữ liệu
// ...
return user;
}
}
@Service
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public boolean isUserValid(int userId) {
User user = userDao.getUser(userId);
return user != null && user.getStatus() == UserStatus.ACTIVE;
}
}
public class UserDaoFake implements UserDao {
private Map<Integer, User> users = new HashMap<>();
public void addUser(User user) {
users.put(user.getId(), user);
}
@Override
public User getUser(int userId) {
return users.get(userId);
}
}
public class UserServiceTest {
private UserDaoFake userDaoFake;
private UserService userService;
@Before
public void setUp() {
userDaoFake = new UserDaoFake();
userService = new UserService(userDaoFake);
}
@Test
public void testIsValidUser() {
// Tạo một đối tượng User và thêm vào UserDaoFake
User user = new User(1, "John", "john@example.com", UserStatus.ACTIVE);
userDaoFake.addUser(user);
// Gọi phương thức cần kiểm tra
boolean result = userService.isUserValid(1);
// Kiểm tra kết quả trả về
assertTrue(result);
}
}
Ở đây, chúng ta sử dụng một đối tượng giả UserDaoFake để thay thế cho đối tượng thực UserDaoImpl. Đối tượng UserDaoFake này chỉ đơn giản là lưu trữ các đối tượng User trong một bản đồ để có thể trả về một đối tượng User cụ thể khi phương thức getUser được gọi. Khi chúng ta kiểm tra phương thức isValidUser của lớp UserService, chúng ta sử dụng đối tượng UserDaoFake này để thực hiện việc lấy dữ liệu và kiểm tra kết quả trả về.
Tạm kết
Hi vọng qua bài viết này bạn đọc đã có cái nhìn rõ ràng hơn về các khái niệm đã nhắc đến trong tiêu đề. Chúc vui
All rights reserved