- vừa được xem lúc

TDD và BDD là gì? Cái nào tốt hơn?

0 0 2

Người đăng: Vinh Phạm

Theo Viblo Asia

Giải thích về TDD và BDD

TDD = Test-Driven Development (Phát triển hướng kiểm thử) BDD = Behavior-Driven Development (Phát triển hướng hành vi)

Phát triển hướng hành vi (BDD)

BDD xoay quanh tư duy sau: Đừng kiểm thử mã nguồn. Hãy kiểm thử hành vi.

Đây là một sự chuyển đổi về cách tiếp cận kiểm thử. Vì vậy, trong BDD, một số thuật ngữ mới được giới thiệu:

  • Test suites (bộ kiểm thử) trở thành specifications (đặc tả),
  • Test cases (các ca kiểm thử) trở thành scenarios (tình huống),
  • Chúng ta không kiểm thử mã, mà xác minh hành vi.

Hãy làm rõ bằng một ví dụ sau đây!

Ví dụ Java

Nếu bạn không quen với Java, hãy xem trong repo để tìm phiên bản bằng ngôn ngữ khác (đã có: Java, Python, JavaScript, C#, Ruby, Go).

public class UsernameValidator { public boolean isValid(String username) { if (isTooShort(username)) { return false; } if (isTooLong(username)) { return false; } if (containsIllegalChars(username)) { return false; } return true; } boolean isTooShort(String username) { return username.length() < 3; } boolean isTooLong(String username) { return username.length() > 20; } // allows only alphanumeric and underscores boolean containsIllegalChars(String username) { return !username.matches("^[a-zA-Z0-9_]+$"); }
}

Lớp UsernameValidator kiểm tra xem một tên người dùng có hợp lệ không (từ 3 đến 20 ký tự, chỉ chứa chữ, số và _). Trả về true nếu vượt qua tất cả các kiểm tra, ngược lại false.

Làm sao để kiểm thử nó? Nếu ta kiểm thử mã nguồn có làm đúng như nó làm hay không, thì sẽ như sau:

@Test
public void testIsValidUsername() { // create spy / mock UsernameValidator validator = spy(new UsernameValidator()); String username = "User@123"; boolean result = validator.isValidUsername(username); // Check if all methods were called with the right input verify(validator).isTooShort(username); verify(validator).isTooLong(username); verify(validator).containsIllegalCharacters(username); // Now check if they return the correct thing assertFalse(validator.isTooShort(username)); assertFalse(validator.isTooLong(username)); assertTrue(validator.containsIllegalCharacters(username));
}

Cách này không tốt. Nếu sau này ta thay đổi logic trong isValidUsername, ví dụ thay isTooShort()isTooLong() bằng isLengthAllowed()?

=> Kiểm thử sẽ hỏng. Vì nó gắn chặt với cách hiện thực. Điều này không tốt chút nào.

Trong BDD, ta chỉ xác minh hành vi mong muốn. Ví dụ:

@Test
void shouldAcceptValidUsernames() { // Examples of valid usernames assertTrue(validator.isValidUsername("abc")); assertTrue(validator.isValidUsername("user123")); ...
} @Test
void shouldRejectTooShortUsernames() { // Examples of too short usernames assertFalse(validator.isValidUsername("")); assertFalse(validator.isValidUsername("ab")); ...
} @Test
void shouldRejectTooLongUsernames() { // Examples of too long usernames assertFalse(validator.isValidUsername("abcdefghijklmnopqrstuvwxyz")); ...
} @Test
void shouldRejectUsernamesWithIllegalChars() { // Examples of usernames with illegal chars assertFalse(validator.isValidUsername("user@name")); assertFalse(validator.isValidUsername("special$chars")); ...
}

Cách này tốt hơn rất nhiều. Nếu thay đổi cách cài đặt bên trong, kiểm thử vẫn chạy tốt miễn là hành vi không đổi.

Việc cài đặt không còn quan trọng, ta chỉ đặc tả hành vi mong muốn. Vì vậy, trong BDD, không gọi là “bộ kiểm thử” mà là “đặc tả” (specification).

Tất nhiên ví dụ trên đơn giản và chưa bao quát hết BDD, nhưng nó làm rõ điểm cốt lõi: kiểm thử mã vs xác minh hành vi.

Có phải chỉ là công cụ?

Nhiều người nghĩ BDD là viết bằng cú pháp Gherkin với Cucumber hay SpecFlow:

Feature: User login Scenario: Successful login Given a user with valid credentials When the user submits login information Then they should be authenticated and redirected to the dashboard

Dù những công cụ này rất hữu ích, BDD không giới hạn ở chúng. BDD là về hành vi, không phải công cụ. Bạn có thể áp dụng BDD với các công cụ đó, hoặc không cần công cụ cũng được.

Phát triển hướng kiểm thử (TDD)

TDD đơn giản là: Viết kiểm thử trước, trước cả khi viết code.

Tức là bạn viết một kiểm thử cho một tính năng chưa tồn tại. Và dĩ nhiên, nó sẽ thất bại. Điều này nghe có vẻ lạ lúc đầu, nhưng TDD tuân theo một chu trình đơn giản:

Red-Green-Refactor

  • Red: Viết một kiểm thử thất bại mô tả chức năng mong muốn.
  • Green: Viết ít mã nhất để kiểm thử đó thành công.
  • Refactor: Cải thiện mã (và kiểm thử nếu cần) trong khi vẫn giữ tất cả kiểm thử thành công.

Chu trình này đảm bảo mọi đoạn mã đều có lý do tồn tại, giúp giảm lỗi và tăng độ tin cậy.

Ba luật của TDD (Uncle Bob)

  • Robert C. Martin (Uncle Bob) đã tổng kết TDD thành 3 luật:
  • Không được viết mã thực tế trừ khi để làm cho một kiểm thử đơn vị thất bại trở thành thành công.
  • Không được viết quá nhiều kiểm thử đơn vị, chỉ đủ để nó thất bại (bao gồm lỗi biên dịch).
  • Không được viết quá nhiều mã thực tế, chỉ đủ để kiểm thử hiện tại thành công.

Kết hợp cả TDD + BDD!

TDD và BDD bổ sung cho nhau, và nên dùng cả hai.

  • TDD đảm bảo mã đúng chức năng thông qua kiểm thử thất bại và chu trình Red-Green-Refactor.
  • BDD đảm bảo kiểm thử tập trung vào hành vi mong muốn, không phụ thuộc vào cách hiện thực.

Hãy viết kiểm thử theo phong cách TDD để phát triển từng bước nhỏ, sau đó dùng tư duy BDD để viết chúng như những kịch bản kiểm thử rõ ràng, tập trung vào kết quả.

Kết quả mang lại là mã nguồn:

  • Chính xác: Nhờ kiểm thử nghiêm ngặt.
  • Dễ bảo trì: Kiểm thử không phụ thuộc vào cách hiện thực.
  • Thiết kế tốt: Viết kiểm thử trước thúc đẩy sự tách biệt và module hóa.

Một ví dụ khác về BDD

Không theo BDD:

@Test
public void testHandleMessage() { Publisher publisher = new Publisher(); List<BuilderList> builderLists = publisher.getBuilderLists(); List<Log> logs = publisher.getLogs(); Message message = new Message("test"); publisher.handleMessage(message); // Verify build was created assertEquals(1, builderLists.size()); BuilderList lastBuild = getLastBuild(builderLists); assertEquals("test", lastBuild.getName()); assertEquals(2, logs.size());
}

Theo BDD:

@Test
public void shouldGenerateAsyncMessagesFromInterface() { Interface messageInterface = Interfaces.createFrom(SimpleMessageService.class); PublisherInterface publisher = new PublisherInterface(messageInterface, transport); // When we invoke a method on the interface SimpleMessageService service = publisher.createPublisher(); service.sendMessage("Hello"); // Then a message should be sent through the transport verify(transport).send(argThat(message -> message.getMethod().equals("sendMessage") && message.getArguments().get(0).equals("Hello") ));
}

Cảm ơn các bạn đã theo dõi!

Bình luận

Bài viết tương tự

- vừa được xem lúc

The Twelve-Factor App, cẩm nang gối đầu giường trong xây dựng application (Phần 1)

Giới thiệu. Ngày nay các phần mềm được triển khai dưới dạng các dịch vụ, chúng được gọi là các web apps hay software-as-a-service (SaaS).

0 0 38

- vừa được xem lúc

8 Sai lầm phổ biến khi lập trình Android

1. Hard code.

0 0 199

- vừa được xem lúc

Popular interview question: What is the difference between Process and Thread? 10 seconds a day

Video được đăng tại channel Tips Javascript

0 0 38

- vừa được xem lúc

Thuật toán và ứng dụng - P1

Mục đích series. . Những bài toán gắn liền với thực tế. Từ đó thấy được tầm quan trọng của thuật toán trong lập trình.

0 0 42

- vừa được xem lúc

Tác dụng của Docker trong quá trình học tập

Docker bây giờ gần như là kiến thức bắt buộc đối với các anh em Dev và Devops, nhưng mà đối với sinh viên IT nói chung vẫn còn khá mơ hồ và không biết tác dụng thực tế của nó. Hôm nay mình sẽ chia sẻ

0 0 46

- vừa được xem lúc

Làm giàu trong ngành IT

Hầu như mọi người đều đi làm để kiếm tiền, ít người đi làm vì thấy cái nghề đó thú vị lắm. Bây giờ vất cho mình 100 tỷ bảo mình bỏ nghề thì mình cũng bỏ thôi.

0 0 48