Cập nhật lần cuối vào 09/10/2023 bởi Phạm Mạnh Cường
Sự phát triển của phát triển Agile đã đưa ra nhiều phương pháp thực tiễn để cung cấp phần mềm chất lượng ở tốc độ cao. Test-driven development (TDD) hiện được công nhận là một phương pháp tiếp cận hiệu quả mang lại kết quả tích cực.
- Test-driven development (TDD) là gì?
- Lợi ích của Test-driven development (TDD)
- Các cấp độ của Test-driven development (TDD)
- 2 phong cách triển khai Test-driven development (TDD) phổ biến
- Những lỗi sai thường gặp khi ứng dụng Test-driven development (TDD)
- Framework sử dụng cho Test-driven development (TDD)
- Ví dụ về Test-driven development (TDD)
Test-driven development (TDD) là gì?
Test-driven development (TDD) là gì?
Test-driven development (TDD) là một phương pháp phát triển phần mềm tập trung vào chu trình phát triển lặp đi lặp lại, trong đó nhấn mạnh vào việc viết các trường hợp thử nghiệm trước khi tính năng hoặc chức năng thực tế được viết.
TDD sử dụng sự lặp lại của các chu kỳ phát triển ngắn. Nó kết hợp xây dựng và thử nghiệm. Quá trình này không chỉ giúp đảm bảo tính chính xác của mã — mà còn giúp gián tiếp phát triển thiết kế và kiến trúc của dự án.
Quy trình Test-driven development (TDD)
Test-driven development (TDD) thường tuân theo chu trình “Red-Green-Refactor”:
- Thêm một thử nghiệm vào bộ thử nghiệm (test suit)
- ( Red ) Chạy tất cả các bài kiểm tra để đảm bảo bài kiểm tra mới thất bại
- ( Green ) Chỉ viết đủ mã để vượt qua bài kiểm tra duy nhất đó
- Chạy tất cả các bài kiểm tra
- ( Refactor ) Cải thiện mã ban đầu trong khi vẫn giữ cho các bài kiểm tra luôn xanh
- Lặp lại
Quá trình này nghe có vẻ chậm và thường có thể diễn ra trong thời gian ngắn nhưng nó sẽ cải thiện chất lượng của dự án phần mềm về lâu dài. Việc có phạm vi kiểm tra đầy đủ đóng vai trò như một biện pháp bảo vệ để bạn không vô tình thay đổi chức năng. Việc phát hiện lỗi cục bộ từ bộ thử nghiệm của bạn sẽ tốt hơn nhiều so với phát hiện lỗi từ khách hàng trong quá trình sản xuất.
Cuối cùng, bộ thử nghiệm có thể gói gọn những kỳ vọng của dự án phần mềm của bạn để các bên liên quan trong dự án có thể hiểu rõ hơn về dự án.
Lịch sử ra đời
Mặc dù ý tưởng xây dựng thử nghiệm trước khi lập trình không phải là ý tưởng ban đầu của cộng đồng Agile, nhưng Test-driven development (TDD) đã tạo nên một bước đột phá khi nó kết hợp ý tưởng đó với ý tưởng “thử nghiệm dành cho nhà phát triển”, mang lại cho nhà phát triển thử nghiệm sự tôn trọng mới.
1976: xuất bản cuốn “Software Reliability” của Glenford Myers, trong đó tuyên bố như một “tiên đề” rằng “nhà phát triển không bao giờ nên kiểm tra mã của chính họ” (Dark Ages of Developer testing)
1990: kỷ luật kiểm thử bị chi phối bởi các kỹ thuật “hộp đen”, đặc biệt là dưới dạng các công cụ kiểm thử “bắt và phát lại”
1991: độc lập tạo ra một khung thử nghiệm tại Taligent với những điểm tương đồng nổi bật với SUnit
1994: Kent Beck viết framework thử nghiệm SUnit cho Smalltalk
1998: bài viết về Extreme Programming đề cập rằng “chúng tôi thường viết bài kiểm thử trước”
1998 đến 2002: “Test First” được xây dựng thành “Test Driven”, đặc biệt trên C2.com Wiki
2000: Mock Object là một trong những kỹ thuật mới được phát triển trong thời kỳ đó
2003: xuất bản cuốn “ Test-driven development: By Example ” của Kent Beck
Đến năm 2006, Test-driven development (TDD) là một môn học tương đối trưởng thành và đã bắt đầu khuyến khích những đổi mới tiếp theo bắt nguồn từ nó, chẳng hạn như ATDD hoặc BDD.
Cấp độ kĩ năng
Beginner:
- có thể viết bài kiểm tra đơn vị trước khi viết mã tương ứng
- có thể viết mã đủ để vượt qua bài kiểm tra thất bại
Intermediate:
- thực hành “test-driven development”: khi tìm thấy lỗi, hãy viết bài kiểm tra để tìm ra lỗi trước khi sửa
- có thể phân tách một tính năng của chương trình ghép thành một chuỗi các bài kiểm tra đơn vị để viết
- biết và kể tên được một số thủ thuật hướng dẫn viết bài kiểm tra (ví dụ “khi kiểm tra thuật toán đệ quy trước tiên hãy viết bài kiểm tra cho trường hợp kết thúc đệ quy”)
- có thể tính ra các yếu tố có thể tái sử dụng từ các bài kiểm tra đơn vị hiện có, mang lại các công cụ kiểm tra theo tình huống cụ thể
Advanced:
- có thể xây dựng “lộ trình” các bài kiểm tra đơn vị theo kế hoạch cho các tính năng vĩ mô (và sửa đổi nó nếu cần thiết)
- có thể “kiểm thử” nhiều mô hình thiết kế khác nhau: hướng đối tượng, chức năng, hướng sự kiện
- có thể “kiểm thử” nhiều lĩnh vực kỹ thuật khác nhau: tính toán, giao diện người dùng, truy cập dữ liệu liên tục, v.v.
Lợi ích của Test-driven development (TDD)
Test-driven development (TDD) khuyến khích viết mã có thể kiểm tra được, liên kết lỏng lẻo và có xu hướng mang tính mô-đun hơn. Vì mã mô-đun có cấu trúc tốt dễ viết, gỡ lỗi, hiểu, bảo trì và tái sử dụng hơn nên TDD giúp:
- Giảm chi phí
- Làm cho việc tái cấu trúc và viết lại dễ dàng hơn và nhanh hơn (“làm cho nó hoạt động” với các giai đoạn Red và Green, sau đó Refactor “để làm cho đúng”)
- Hợp lý hóa quá trình triển khai dự án
- Ngăn chặn lỗi
- Cải thiện sự hợp tác tổng thể của nhóm
- Tăng sự tự tin rằng mã hoạt động như mong đợi
- Cải thiện các mã
- Loại bỏ nỗi sợ thay đổi
Các cấp độ của Test-driven development (TDD)
Mức chấp nhận (Acceptance TDD/ATDD)
Bài kiểm thử này được tạo ra bởi sự cộng tác của 3 bên: Khách hàng – Lập trình viên – Kiểm thử viên.
Các thử nghiệm chấp nhận này thể hiện quan điểm của người dùng và hoạt động như một dạng yêu cầu để mô tả cách hệ thống sẽ hoạt động cũng như phục vụ như một cách để xác minh rằng hệ thống hoạt động như dự định.
ATDD còn được gọi là Behavioral Driven Development (BDD).
Mức lập trình (Developer/TDD)
Bài kiểm thử này nhằm mục đích kiểm thử một đoạn mã, còn được gọi là unit test. Các thử nghiệm này tập trung vào chức năng nhỏ của dự án. Developer TDD còn được gọi là TDD.
2 phong cách triển khai Test-driven development (TDD) phổ biến
Có hai cách tiếp cận chính đối với TDD – Inside Out và Outside In.
Inside Out
Inside Out là cách kiểm thử đi từ các kiểm thử nhỏ, tiếp cận từng chút một, hướng tới người dùng, có các ưu điểm như:
- Tập trung: Chúng ta chỉ tập trung vào một lớp (layer) ứng dụng tại một thời điểm
- Song song hóa: Công việc có thể được thực hiện trên các lớp ngay cả khi một lớp bị chặn.
- Phản hồi nhanh: Vì phạm vi thử nghiệm là từng lớp cụ thể nên vòng phản hồi của “Red, Green, Refactor” rất nhanh và toàn bộ lớp có thể được viết và vận chuyển trước khi chuyển sang lớp tiếp theo.
Tuy nhiên, mức độ chi tiết của Inside Out cũng có mang lại một vài nhược điểm:
- Độ cứng: Nếu mô hình dữ liệu thay đổi, tất cả các lớp sẽ cần được thay đổi để phản ánh cách hiểu mới.
- Phân phối chậm: Mặc dù mỗi lớp có thể được triển khai độc lập (từ dưới lên), nhưng chỉ khi tất cả công việc được thực hiện thì người dùng mới có thể nhận được phản hồi.
- Cám dỗ nặng nề: Vì chúng ta bắt đầu từ phần dưới cùng và không biết chính xác dữ liệu sẽ được sử dụng cho mục đích gì về sau, nên việc trả về toàn bộ mô hình dữ liệu và để giao diện người dùng hiển thị những gì nó cần là rất hấp dẫn.
- Đầu tư: Nếu tính năng bị thay đổi hoặc bị hủy, có thể có rất nhiều chi tiết cần loại bỏ vì công việc chưa hoàn thành đã được chuyển đi.
Inside Out đặc biệt hữu ích khi bạn không chắc chắn về thiết kế cuối cùng —hệ thống hoặc các thành phần trực quan. Nó cũng có thể hữu ích khi bạn không biết mọi thứ được kết nối với nhau như thế nào, vì bạn có thể bắt đầu với cấp độ quen thuộc nhất và xây dựng từ đó.
Inside Out cũng thực sự tốt khi bạn biết giao diện là gì (hoặc đã được giao một giao diện để bạn sử dụng). Vì phạm vi được giới hạn ở một lớp duy nhất tại một thời điểm nên việc triển khai và vượt qua các bài kiểm tra sẽ dễ dàng hơn.
Ví dụ: nếu bạn đang làm việc trên một điểm cuối mà hai nhóm khác nhau cần tận dụng, tốt nhất bạn nên xây dựng và vận chuyển tính năng theo từng lớp để đảm bảo các nhóm khác không bị chặn.
Một trường hợp sử dụng khác của từ trong ra ngoài là khi dự án được định hướng theo miền cụ thể.
Ví dụ: câu chuyện là “cho tôi xem toàn bộ đối tượng dữ liệu này trên một trang”. Một cái gì đó tương tự mà không có bất kỳ loại thao tác dữ liệu nào là một ứng cử viên tuyệt vời cho từ trong ra ngoài, vì chính mô hình dữ liệu sẽ xác định những gì được hiển thị trên trang.
Outside In
Bắt đầu từ cuối—tức là “người dùng cuối”.
- Ranh giới miền: Việc đặt tên và cấu trúc dữ liệu được xây dựng để phù hợp nhất với giao diện người dùng, do đó, mọi hoạt động phá hủy hoặc ánh xạ đều được đẩy vào giao diện phía sau (nơi nó thuộc về!).
- Nhẹ: Chúng ta chỉ kiểm tra dữ liệu hoặc mã mà chúng ta biết là cần thiết cho người dùng, vì vậy thật dễ dàng để tránh mã YAGNI (bạn sẽ không cần nó).
- Khả năng lặp lại: Các tính năng có thể dễ dàng mở rộng vì chúng được xây dựng từ trên xuống dưới.
- Tính độc lập: Mỗi lớp ứng dụng được xây dựng riêng biệt (sử dụng mô phỏng và sơ khai để kiểm tra), do đó chúng có thể được ghép nối lỏng lẻo.
Việc tập trung vào tính năng nói chung có một số đánh đổi, đặc biệt là liên quan đến thời gian:
- Đỏ trong thời gian dài: Kiểm tra tính năng giữ màu đỏ lâu hơn vì chúng sẽ không vượt qua cho đến khi mọi kiểm tra dịch vụ lớp đều có màu xanh lục
- Chậm: Mỗi lớp cần lớp bên dưới được đặt gốc, nghĩa là phải viết rất nhiều mã hỗ trợ chỉ để kiểm tra.
- Kiến thức sẵn có: Việc xây dựng một tính năng mới đòi hỏi kiến thức về các lớp của hệ thống và cách chúng tương tác.
Outside In được sử dụng tốt nhất khi một người đang làm việc trên các câu chuyện theo chiều dọc của người dùng, đặc biệt khi cần có thông tin cụ thể ở giao diện người dùng. Bằng cách tập trung vào trải nghiệm người dùng trước tiên, chúng ta bảo vệ mình khỏi việc viết mã không liên quan.
Từ ngoài vào trong cũng hữu ích khi bạn không chắc chắn mình sẽ lấy dữ liệu ở đâu. Nó kết thúc là phần cuối cùng của quá trình! Điều này cũng có nghĩa là các lớp của ứng dụng có thể trao đổi nguồn dữ liệu một cách dễ dàng và yên tâm rằng phần còn lại của mã sẽ hoạt động như mong đợi.
Cách tiếp cận nào tốt hơn?
Không. Hãy thử từng cái một. Hãy sử dụng chúng khi thích hợp.
Việc sử dụng phương pháp Outside In thường dễ dàng hơn khi làm việc với các ứng dụng phức tạp có số lượng lớn các phần phụ thuộc bên ngoài thay đổi nhanh chóng (ví dụ: các vi dịch vụ). Các ứng dụng nguyên khối, nhỏ hơn thường phù hợp hơn với phương pháp Inside Out.
Cách tiếp cận Outside In cũng có xu hướng hoạt động tốt hơn với các ứng dụng giao diện người dùng vì mã rất gần gũi với người dùng cuối.
Những lỗi sai thường gặp khi ứng dụng Test-driven development (TDD)
Những sai lầm cá nhân điển hình bao gồm:
- Quên chạy thử nghiệm thường xuyên
- Viết quá nhiều bài kiểm tra cùng một lúc
- Viết bài kiểm tra quá lớn hoặc thô
- Viết các bài kiểm tra quá tầm thường, ví dụ như bỏ qua các xác nhận
- Viết bài kiểm tra cho mã tầm thường, ví dụ: trình truy cập
Những cạm bẫy điển hình của nhóm bao gồm:
- Áp dụng một phần – chỉ một số nhà phát triển trong nhóm sử dụng TDD
- Bảo trì bộ thử nghiệm kém – phổ biến nhất là dẫn đến bộ thử nghiệm có thời gian chạy quá dài
- Bộ kiểm thử bị bỏ rơi (tức là hiếm khi hoặc không bao giờ chạy) – đôi khi do bảo trì kém, đôi khi do thay đổi nhóm
Framework sử dụng cho Test-driven development (TDD)
Dựa trên các ngôn ngữ lập trình độc đáo, nhiều khung hỗ trợ phát triển theo hướng thử nghiệm. Dưới đây là một vài cái phổ biến.
- csUnit và NUnit là các khung thử nghiệm đơn vị nguồn mở cho các dự án .NET.
- PyUnit và DocTest: Khung thử nghiệm đơn vị phổ biến cho Python.
- Junit: Công cụ kiểm tra đơn vị được sử dụng rộng rãi cho Java
- TestNG: Một framework thử nghiệm Java phổ biến khác. Khung này khắc phục được những hạn chế của Junit.
- Rspec: Khung thử nghiệm cho các dự án Ruby
Ví dụ về Test-driven development (TDD)
Ở ví dụ này, chúng ta sẽ xác định mật khẩu lớp. Đối với lớp này, chúng ta sẽ cố gắng đáp ứng các điều kiện sau.
Điều kiện để chấp nhận Mật khẩu: Mật khẩu phải có từ 5 đến 10 ký tự.
Đầu tiên trong ví dụ TDD này, chúng ta viết mã đáp ứng tất cả các yêu cầu trên.
Tình huống 1 : Để chạy thử nghiệm, chúng ta tạo lớpPasswordValidator();
Chúng ta sẽ chạy trên lớp TestPassword();
Đầu ra được PASSED như hình bên dưới;
Đầu ra :
Tình huống 2 : Ở đây chúng ta có thể thấy trong phương thức TestPasswordLength() không cần tạo một thể hiện của lớpPasswordValidator. Sơ thẩm có nghĩa là tạo một đối tượng của lớp để tham chiếu các thành viên (biến/phương thức) của lớp đó.
Chúng ta sẽ xóa lớpPasswordValidator pv = newPasswordValidator() khỏi mã. Chúng ta có thể gọi trực tiếp phương thức isValid() bằng PassValidator. IsValid (“Abc123”) .
Vì vậy chúng ta Refactor (đổi code) như sau:
Tình huống 3 : Sau khi tái cấu trúc, đầu ra hiển thị trạng thái không thành công (xem hình ảnh bên dưới), điều này là do chúng ta đã xóa phiên bản. Vì vậy không có tham chiếu nào tới phương thức non –static isValid().
Vì vậy chúng ta cần thay đổi phương thức này bằng cách thêm từ “tĩnh” trước Boolean dưới dạng public static boolean isValid (Mật khẩu chuỗi). Tái cấu trúc lớp PassValidator () để loại bỏ lỗi trên để vượt qua bài kiểm tra.
Đầu ra:
Sau khi thực hiện thay đổi class PassValidator() nếu chúng ta chạy test thì kết quả sẽ là PASSED như hình bên dưới.