Testing On the Toilet: Change-Detector Tests Considered Harmful + α
Testing
24
White

Vu Nhat Minh viết ngày 15/04/2016

Có bao giờ...

Bạn vừa refactor một mẩu code, không động đến business logic của method. Chắc mẩm quả này commit xong ngồi chơi phe phé. Bạn gọi UT lên chạy.

Doom - test fail, vài chục case đỏ lòm lòm. Bạn há hốc mồm cắn răng cắn lợi đi tìm bug.... Một tham số mới được truyền thêm vào method, và giờ bạn phải sửa 100 caller đến method nói trên để test lại chạy đúng. Oops. Thời gian bị lãng phí khi phải sửa cùng một thay đổi vào quá nhiều testcase.

Chúng ta gọi những testcase kiểu đó là những testcase "nhảm". Ví dụ đơn giản nhé

// Production code:
def abs(i: Int)
  return (i < 0) ? i * -1 : i

// Test code:
for (line: String in File(prod_source).read_lines())
  switch (line.number)
    1: assert line.content equals def abs(i: Int)
    2: assert line.content equals   return (i < 0) ? i * -1 : i

Test code được viết theo phong cách .... đọc file production source code rồi assert từng dòng 1.
Cần Lời Giải Thích ??? Đứa nào ngu si viết code kiểu này ?

Bạn có thể thấy ví dụ trên thật nực cười. Testcase ở đây giống như được sản xuất ra một cách máy móc khi đọc production code. Thấy gì quen quen chưa nào :) Tiếp nhé

// Production code:
def process(w: Work)
  firstPart.process(w)
  secondPart.process(w)

// Test code:
part1 = mock(FirstPart)
part2 = mock(SecondPart)
w = Work()
Processor(part1, part2).process(w)
verify_in_order
  was_called part1.process(w)
  was_called part2.process(w)

Testcase thứ 2 này về bản chất cũng không khác testcase đầu tiên là mấy. Viết nó tốn ... ít thời gian nghĩ và về cơ bản sẽ chạy. Test case kiểu này gọi là "change detector". Change detector là lỗi cơ bản khi viết test, khi nó không đem lại mục đích test business logic chính, nhưng làm tăng chi phí khi vận hành và phát triển. Change detector cần phải được viết lại hoặc loại bỏ.


Đến đây là hết bản dịch của bài Testing on the Toilet: Change Detector Considered Harmful của blog Google Testing. Dưới đây là phần của tác giả.

Lời tựa

Nếu bạn đọc đến đây và vẫn nghĩ, "ví dụ thật nhảm nhí và thực tế chẳng ai viết code kiểu này cả", thì nên đọc lại và ngẫm nghĩ thêm lần nữa.

Change detector là lỗi kinh điển mà ngay cả những dev kinh nghiệm cũng dễ mắc phải. Change detector có thể lẩn khuất đâu đó trong số testcase mà bạn đã viết ra. Để nhận biết đâu là Change detector và đâu là không phải, cần phải có cái đầu không xoắn và con mắt tinh tường :sweat_smile:

Không test message lỗi

val thrown = intercept[DatabaseException] {
  userService.createUser()
}
assert(thrown.getMessage === "Connection to DB failed")

Message lỗi là thứ team vận hành sẽ đọc. Message lỗi "chân lý" là message lỗi được ghi trong code. Nếu giả sử message lỗi sai khác so với thiết kế, thì đó là lỗi của dev, không phải lỗi của production code. Khi viết test như thế này đảm bảo xâu Connection to DB failed được copy-paste ra từ production code.

Verify cái gì và không verify cái gì

Verify cái gì lại là một ví dụ hay nữa.

// production code
class UserService(userRepository: UserRepository) {
  def updateName(userId: Int, newName: String): Unit = {
    userRepository.find(userId)
    val updated = user.copy(name = newName)
    userRepository.update(user)
  }
}

// test code
"UserService.updateName" should "update user name" in {
  val userId = 1
  val user = User(userId = userId, name = "Minh")

  val userRepository = Mockito.mock(classOf[UserRepository])
  Mockito.when(userRepository.find(userId)).thenReturn(user)
  Mockito.doNothing.when(userRepository).update(Matchers.any[User])

  val userService = new UserService(userRepository)
  val newName = "batman"
  userService.updateName(userId, newName)

  Mockito.verify(userRepository).find(userId)      // (1)
  val expected = user.copy(name = newName)
  Mockito.verify(userRepository).update(expected)  // (2)
}

Hãy để ý (1) và (2) nhé. 2 trường hợp verify này là khác nhau.

  • (2) có cần thiết hay không ? Rõ ràng là cần thiết vì update() là logic chính của production code. Nếu hàm này không chạy qua thì có nghĩa là production code không chạy đúng.
  • (1) thì thế nào ?

Ý nghĩa của (1) là kiểm tra xem có đúng là find method đã được gọi với userId đã truyền vào hay không. Tuy vậy hành vi này không liên quan trực tiếp đến chuyện ouput "user đã được update hay chưa".

Nói cách khác, kể cả find không được gọi nhưng update đã được gọi cũng được rồi, chẳng sao cả ! Dù bạn nghe có vẻ phi lý, nhưng giả sử tương lai mình dùng một method khác không phải find để tìm user rồi mới update, thì rõ ràng mình sẽ phải sửa cả dòng verify này nữa.

Vậy là ở đây, (1) là thừa/ gây ảnh hưởng đến tương lai/ mang dáng dấp của Change detector nên cần loại bỏ.

Càng giống thật càng tốt

Mình đang nói đến mock object

Mockito.when(userRepository.find(userId)).thenReturn(user)
Mockito.doNothing.when(userRepository).update(Matchers.any[User])

Method find và method update có 1 sự khác biệt cơ bản. find sẽ trả lại các user khác nhau cho các tham số userId khác nhau, còn update chỉ là hàm Unit(void) đối với mọi tham số user.

Có một best practise là: tạo ra các mock object càng giống object thực tiễn càng tốt. Ở đây với cách mock như 2 dòng bên trên, userRepository là một mock object với hành vi rất "giống thật", khi find thì chỉ trả về user khi nhận userId và khi update thì đều doNothing khi nhận bất cứ user nào.

Nếu có thời gian mình sẽ viết tiếp về lý thuyết này. Hẹn gặp các bạn lần sau.
Happy and smart testing!

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Vu Nhat Minh

54 bài viết.
724 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
116 29
Nếu bạn thường vào trang mua sắm của amazon, chắc sẽ chẳng lạ gì với menu Shop by Department. Tốc độ hiển thị nội dung của menu là tức thì so với d...
Vu Nhat Minh viết hơn 2 năm trước
116 29
White
87 4
Lời người dịch Người dịch là một developer , sau khi tìm đọc được bài viết này bằng bản gốc tiếng Anh đã cảm thấy như được "khai sáng" về khả năng...
Vu Nhat Minh viết hơn 2 năm trước
87 4
White
56 5
Đây là phần cuối của một series chuyên về thiết kế UI. Bạn nên đọc (Link) trước khi bắt đầu đọc phần này. Luật số 7: "Ăn trộm" như là một nghệ sỹ...
Vu Nhat Minh viết hơn 2 năm trước
56 5
Bài viết liên quan
White
4 0
1. Định nghĩa Một kế hoạch kiểm thử dự án phần mềm (test plan) là một tài liệu mô tả các mục tiêu, phạm vi, phương pháp tiếp cận, và tập trung vào...
Thiên Hoàng Minh Vũ viết 1 tháng trước
4 0
White
7 2
Khi test tự động có đụng đến DB, thường ta phải tạo rồi xóa DB rất nhiều lần. Do đó nếu lưu DB trên đĩa cứng bình thường thì mỗi lần chạy test phải...
Ngoc Dao viết gần 2 năm trước
7 2
White
5 0
Test framework của RSpec luôn làm tôi bất ngờ với nhiều hàm dường như rất ít được biết đến nhưng khá là hữu dụng. Hôm nay trong khi phỏng vấn một ứ...
Lơi Rệ viết hơn 2 năm trước
5 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
54 bài viết.
724 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!