Use Mock to make Unit Test easy

bài viết How to Write Testable Code có đề cập đế khái niệm Mock. Vậy Mock là gì? Và đặc biệt là các sử dụng nó dể thực hiện Unit Test đơn giản hơn nhé.

MockK-logo

What’s Mock

Mock là một fake class cho phép ta pre-programmed ở bước runtime nhằm giả lập phản ứng của fake class đó với một lời gọi cụ thể nào đó. Ngoài ra Mock còn cho phép ta verify xem các method trong fake class đã được gọi bao nhiêu lần.

Chà! Định nghĩ nghe có vẻ khó hiểu quá!

Mình sẽ ví dụ test case sau:

Test một phương thức truy vấn thông tin khách hàng mua nhiều hơn 30 triệu từ Database

Khi đó ta có các vấn đề sau khi thực hiện Unit Test:

  • Khi Unit Test phải tạo connection tới Database hay sao? Trường hợp cho phép connection xuống Database lỡ có ai đó thay đổi Database thì sao?
  • Làm sao để biết bên trong hàm thực sự truy vấn thông tin khách hàng mua nhiều hơn 30 triệu hay 29 triệu? Số lần gọi truy vấn xuống Database? Vì nếu ta chuẩn bị data trong Database không tốt có thể kết quả trả về của truy vấn 29tr và 30tr là như nhau, cũng như việc ta truy vấn xuống Database 2 lần trong một lần gọi cũng không thể biết vì kết quả trả về là như nhau.

Và khi này Mock xuất hiện như một người hùng và giúp ta giải quyết các vấn đề trên.

Ngoài ví dụ trên Mock còn có thể giúp ta trong các trường hợp sau:

  • Đối tượng cung cấp thông tin không xác định. Như: trả về số random, thời gian hiện tại, thời tiết, nhiệt độ…
  • Đối tượng cung cấp thông tin qua database, web service, rpc, cache…
  • Đối tượng cần tính toán lâu, tốn resource
  • Đối tượng chưa tồn tại hoặc có thể bị thay đổi. Như trong ví dụ trong bài Custom Exception mình có thể thực hiện Unit Test trước phần SalaryTransfer mà không phải đợi một bạn khác làm phần TransferMoney bằng cách Mock kết quả 💪.
  • Các lời gọi liên quan tới context như thread context, security context, scope context…

Note: Có một bài viết chi tiết của Martin Fowler ở đây, có so sánh về StubMock và cũng như cung cấp rất nhiều thông tin khác về Test nên nếu có thời gian các bạn nên đọc qua nhé. Ở đây mình chỉ muốn giới thiệu về Mock thôi.

Mock and Dependency Injection

Như ví dụ đầu ở What’s Mock nếu theo cách code truyền thống class xử lý và class truy xuất DB có sự liên hệ chặt chẻ với nhau dẫn đến muốn test ta cần phải setup một DB thật và data trong DB cũng phải thật. May mắn nhờ có IoC và DJ (xem thêm ở Inversion of Control and Dependency Injection), ta có thể injection class truy xuất DB vào class xử lý trong lúc runtime và injection mock class truy xuất DB trong trường hợp test. Phần tiếp theo sẽ làm rõ hơn ý mình muốn nói.

Example Unit Test with Mock

Ví dụ có yêu cầu sau:

Hiển thị full name của những khách hàng mua nhiều hơn N từ Database với N được input vào.

Từ yêu cầu trên mình cần implement CustomerRepository:

1
2
3
4
5
interface CustomerRepository : CrudRepository<Customer, Long> {

  fun findByTotalBuyPriceGreaterThan(totalBuyPrice: Int): List<Customer>

}

Và từ các Customer nhận về mình map() thành full name như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
class CustomerService(
  private val customerRepository: CustomerRepository
) {

  fun getFullNameCustomerWithTotalBuyPriceGreaterThan(totalBuyPrice: Int) =
    customerRepository
      .findByTotalBuyPriceGreaterThan(totalBuyPrice)
      .map { "${it.firstName} ${it.lastName}" }

}

Và setup mock

1
2
3
4
5
6
7
private val customerRepository: CustomerRepository = mockk()
private val customerService = CustomerService(customerRepository)

@AfterEach
fun tearDown() {
  clearMocks(customerRepository)
}

Và đây là một trong các test case:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
fun `test getFullNameCustomerWithTotalBuyPriceGreaterThan return one customer`() {
  val totalBuyPrice = 3_000

  every {
    customerRepository.findByTotalBuyPriceGreaterThan(3_000)
  } returns listOf(
    Customer(firstName = "Tri", lastName = "Le")
  )

  assertEquals(
    listOf("Tri Le"),
    customerService.getFullNameCustomerWithTotalBuyPriceGreaterThan(3_000)
  )

  verify(exactly = 1) { customerRepository.findByTotalBuyPriceGreaterThan(totalBuyPrice) }

}

Một test case tiêu chuẩn sẽ bao gồm 3 phần:

  1. Setup: từ dòng 5-9, để setup cho mock biết được cần trả về Customer(firstName = "Tri", lastName = "Le") khi customerRepository.findByTotalBuyPriceGreaterThan(3_000) được gọi

  2. Assert: từ dòng 11-14 so sánh kết quả mong muốn với kết quả thực tế.

  3. Verify: dòng 16 kiểm tra xem thật sự mock đã được gọi hay không, được gọi với các tham số như thế nào cũng như số lần được gọi. Một số bạn hay bỏ qua bước này nhưng thật ra nó rất quan trọng vì sẽ tránh được các tình huống như code gọi khác số lần mong muốn, hãy tưởng tượng như ở ví dụ bài Clean Code with Exception chuyển lương nhiều lần cho nhân viên thì như thế nào 😓 hoặc lời gọi cần được thực hiện nhưng lại không tham gia vào kết quả trả về 👿.

Note 1: Ở đây mình sử dụng Spring JPA nên không cần phải có implement class. Các trường hợp khác các bạn cần implement concrete classSpring sẽ tự động biết mà inject đúng concrete class khi ứng dụng được khởi chạy. Mình sẽ có có bài chi tiết hơn nha.

Note 2: Phần code sample mình update vào repo này nhé.

Kết: Với sự giúp sức của Mock việc Unit Test trở nên đơn giản và đáng tin cậy hơn rất nhiều.

Cảm ơn các bạn đã đọc tới đây và mong các bạn sẽ thích bài viết này.

Reference articles
updatedupdated2021-09-192021-09-19
Load Comments?