Giới thiệu mô hình TDD trong lập trình, và áp dụng với framework Yii2
Để hiểu rõ hơn về TDD và cách sử dụng nó trong thực tế, bài viết này sẽ có 2 phần:
- Phần 1: Giới thiệu về TDD, cách hoạt động, nhưng lợi ích và vấn đề gặp phải khi sử dụng TDD
- Phần 2: Sử dụng TDD với framework Laravel
1. TDD là gì?
Phát triển phần mềm hướng kiểm thử (Test-Driven Development) là một phương pháp để phát triển phần mềm trong đó kết hợp Test First Development và Refactoring. Mục tiêu quan trọng nhất của TDD là hãy nghĩ về thiết kế trước khi viết mã nguồn cho chức năng. Một quan điểm khác lại cho rằng TDD là một kỹ thuật lập trình. Nhưng nhìn chung, mục tiêu của TDD là viết mã nguồn sáng sủa, rõ ràng và có thể chạy được.
TDD bắt đầu với việc thiết kế và phát triển các test (kiểm thử) cho mỗi function nhỏ của ứng dụng. Việc viết code mới cho các function này chỉ cần thực hiện nếu test của function đó bị fail, điều này giúp tránh việc trùng code trong quá trình phát triển ứng dụng.
TDD là một quá trình phát triển và chạy kiểm thử một cách tự động trước khi phát triển ứng dụng thực tế. Vì vậy, TDD đôi khi còn được gọi là Test First Development.
2. Các bước thực hiện TDD
B1. Code 1 test mới
Việc thêm một tính năng mới sẽ bắt đầu bằng việc việc một test đáp ứng được các yêu cầu về nghiệp vụ. Ở bước này, lập trình viên có thể dựa vào tài liệu nghiệp vụ, hoặc các user-story để có thể viết test đáp ứng được yêu cầu của tính năng. Lợi ích của việc viết test trước là lập trình viên có thể tập trung vào các yêu cầu của tính năng trước khi viết mã nguồn (code). Điều này trái ngược với việc phát triển thông thường là sẽ viết code trước, sau đó mới viết test.
B2. Chạy tất cả các test hiện có.
Các đoạn test mới được viết ( chưa có code) khi chạy sẽ thất bại (fail). Còn các test khác thì sẽ thành công (pass), việc chạy tất cả test để đảm bảo rằng, việc thêm test mới không có ảnh hưởng gì đến các tính năng đang chạy đúng trước đó.
B3. Viết code cơ bản nhất để có thể pass được test mới viết
Code chưa tối ưu hoặc việc hard-code ở đây vẫn được chấp nhận, miễn là code này pass được test mới viết. Code sẽ được tối ưu ở bước 5, vì vậy không nên viết quá phức tạp ở bước này để tránh mất quá nhiều thời gian không cần thiết.
B4. Chạy lại các test hiện có
Ở bước chạy lại tất cả test này, tất cả các test sẽ pass, nếu có bất kỳ cái nào bị fail, thì chúng ta cần sửa code cho tới khi chúng được pass. Việc này để đảm bảo rằng code mới đáp ứng được các yêu cầu logic của test, đồng thời không ảnh hưởng đến các tính năng hiện có.
B5. Tối ưu code nếu cần, chạy lại test sau mỗi lần tối ưu code để đảm bảo chức năng chạy bình thường
Code thường được tối ưu để dễ đọc và bảo trì. Đặc biệt, các phần hard-code cần được xóa bỏ. Chạy lại các test sau mỗi lần tối ưu để chắc chắn rằng không có chức năng nào của hệ thống bị lỗi.
Một số ví dụ về tối ưu:
- Chuyển code đến vị trí hợp lý
- Xóa code trùng lặp
- Thêm tài liệu, hướng dẫn cho các đoạn code
- Chia nhỏ các phương thức thành các phần nhỏ hơn
Lặp lại các bước trên
Các bước ở trên nên được lặp lại cho mỗi phần mới của chức năng. Test nên được chia nhỏ để thực hiện, và cần chạy test thường xuyên. Với cách này, nếu một số test bị lỗi, thì lập trình viên có thể xử lý đơn giản bằng cách hoàn tác lại code cũ thay vì phải đi điều tra và thêm code để sửa lỗi.
Khi sử dụng các thư viện bên ngoài, không nên viết các test quá nhỏ để kiểm thử việc chức năng của thư viên đã chạy đúng chưa, điều này là không cần thiết, trừ khi có lý do nào đó để tin rằng thư viện có lỗi hoặc không đủ tính năng để phục vụ tất cả các nhu cầu của phần mềm đang được phát triển.
3. Lợi ích khi sử dụng TDD
Có nhiều lợi ích được mang lại khi thực hiện phát triển phần mềm theo TDD:
-
Nắm rõ được các yêu cầu của tính năng: Khi phát triển tính năng mới, thường sẽ có một danh sách các yêu cầu nghiệp vụ hoặc tiêu chí chấp nhận đi kèm. Từ những thông tin này, chúng ta sẽ viết các test đáp ứng nó, khi đó, việc việc code để pass các test là cũng sẽ đáp ứng được các yêu cầu về nghiệp vụ của tính năng đó.
-
Tập trung: TDD sẽ giúp lập trình viên tập trung vào các tính năng nhỏ, và sẽ năng suất hơn khi code. Khi một test thất bại, chúng ta sẽ tập trung nghĩ cách để cho nó thành công, điều đó bắt buộc bạn nghĩ tới những tính năng nhỏ nhất vào một thời điểm hơn là nghĩ về tổng thể hệ thống, và sau đó có thể tập trung vào việc làm cho test thành công, hơn là dành nhiều thời gian để nhìn vào bức tranh tổng thể ngay từ lúc đầu, điều mà sẽ dẫn đến nhiều bugs và nhiều thời gian dành cho việc phát triển.
-
Interfaces: Bởi vì việc viết test cho một phần của chức năng, nên chúng ta cần nghĩ tới các public interfaces hơn mà các đoạn code khác trong hệ thống cần tích hợp cùng. Chúng ta sẽ chưa cần nghĩ tới những private methods xử lý ra sao. Dưới góc nhìn của một đoạn test, chúng ta sẽ chỉ viết những đoạn gọi phương thức để test public methods. Điều này có nghĩa là code sẽ dễ đọc và dễ hiểu hơn.
-
Code gọn gàng hơn: Đoạn test được viết sẽ chỉ giao tiếp với public methods, vì vậy chúng ta sẽ có cái nhìn rõ ràng hơn về method nào cần khai báo là private, có nghĩa là sẽ không vô tình để lộ ra method mà không cần thiết phải là public. Nếu không thực hành TDD, và đặt một method là public, có nghĩa là có thể chúng ta sẽ phải hỗ trợ chức năng đó trong tương lai và vô tình đã tạo thêm việc cho mình vì một method mà chỉ nên sử dụng nội bộ của một class.
-
Dependencies: Khi viết test, chúng ta có thể làm giả (mock) những sự phụ thuộc (dependencies) mà không phải lo lắng về việc chúng sẽ làm gì ở trong hệ thống, điều mà sẽ giúp tập trung hơn vào logic của class đang viết. Một điểm cộng nữa đó là những dependencies mà được mock sẽ giúp test chạy nhanh hơn vì những depedencies thật sự như filesystem, network hay database sẽ không cần được sử đụng đến.
-
An toàn hơn khi refactor: Mỗi khi refactor code, chúng ta chỉ cần chạy lại các đoạn test và pass chúng, điều đó chứng tỏ là các đoạn code mới được refactor hoạt động tốt và không có ảnh hưởng đến các logic đang hoạt động trước đó.
-
Ít bugs hơn: TDD sẽ sinh ra nhiều test, và dẫn tới thời gian chạy test lâu hơn. Tuy nhiên, với độ bao phủ test tốt, chúng ta có thể tiết kiệm thời gian dùng để sửa lỗi (bug). Điều này có nghĩa rằng nếu có một bug xuất hiện, chúng ta vẫn có thể viết một đoạn test trước khi tìm cách sửa nó, điều đó sẽ đảm bảo là bug sẽ không xuất hiện nữa. Điều này cũng giúp chỉ ra rõ nguyên nhân gây ra bugs và dễ dàng tái hiện lại bugs.
-
Tăng lợi nhuận: Ở giai đoạn đầu của dự án, việc sử dụng TDD sẽ tốn nhiều thời gian và chi phí hơn việc không cần viết test trước. Tuy nhiên, nó sẽ giảm được rất nhiều thời gian và chi phí cho việc fix bugs ở giữa và cuối dự án. Ngoài ra, TDD còn giảm chi phí cho việc thay đổi vì những test được viết trước sẽ đảm bảo rằng những thay đổi mới sẽ không phá vỡ những chức năng đã có.
-
Tài liệu mới nhất: Test có thể đóng vai trò như tài liệu được cập nhật liên tục cho một lập trình viên. Nếu chúng ta không rõ một class hay một library hoạt động ra sao, hãy đọc qua test của chúng. Với TDD, test thường được viết cho nhiều trường hợp, vì vậy có thể dễ dàng hiểu được yêu cầu của input mong muốn cho một method và kỳ vọng output thông qua những sự so sánh trong các đoạn test.
Tất nhiên nó cũng có một số nhược điểm:
- Không dễ để bắt đầu với TDD. Nó đòi hỏi lập trình viên phải biết rất nhiều thực hành và kỹ thuật mới. Ví dụ: Unit test, mocks, assertions .v.v
- Phải mất nhiều thời gian hơn khi phát triển, vì lập trình viên cần viết nhiều đoạn test đáp ứng được yêu cầu tính năng, sau đó mới thêm các code mới
Nhưng TDD vẫn có nhiều mặt tích cực hơn và rất ít nhược điểm. TDD thực sự cần thiết nếu chúng ta muốn xây dựng một dự án với mã nguồn của mình có thể dễ bảo trì và phát triển trong tương lai. Còn nếu dự án chỉ đang thử nghiệm thì chúng ta có thể không cần sử dụng TDD.
4. Acceptance TDD and Developer TDD
Có 2 level của TDD:
- Acceptance TDD (ATDD): Với ATDD, chúng ta sẽ viết các kiểm thử chấp nhận (acceptance test). Kiểm thử này đáp ứng yêu cầu về đặc điểm kỹ thuật hoặc thỏa mãn hành vi của hệ thống. Sau đó, viết đầy đủ code để hoàn thành việc kiểm thử chấp nhận. Kiểm thử chấp nhận tập trung vào các hành vi tổng thể của hệ thống nên ATDD cũng được biết đến với tên gọi Behavioral Driven Development (BDD)
- Developer TDD: Với Developer TDD, chúng ta sẽ viết các đoạn test như: unit-test và sau đó viết code để hoàn thành các unit-test đó. Các unit-test tập trung vào mỗi chức năng nhỏ trên hệ thống. Developer TDD được gọi đơn giản là TDD. Mục tiêu chính của ATDD và TDD là xác định các yêu cầu chi tiết, có thể thực thi được để giải quyết các vấn đề cần thiết và hiệu quả nhất, giúp tăng hiệu suất trong quá trình phát triển phần mềm.
5. Một số vấn đề và các lỗi thường gặp khi sử dụng TDD
Trong quá trình sử dụng TDD, chúng ta cần tránh một số hiểu lầm như:
- TDD không phải tập trung về việc kiểm thử (testing) hay là về thiết kế (design)
- TDD không có nghĩa là "viết một vài kịch bản test rồi xây dựng một hệ thống sao cho nó pass các kịch bản test này "
- TDD không có nghĩa là thực hiện test nhiều hơn
Và cá nhân lập trình viên cũng cần tránh một số lỗi như:
- Không quan tâm đến các test bị thất bại
- Thực hiện tối ưu code trong lúc viết code để pass các đoạn test
- Quên đi thao tác tối ưu sau khi viết code để pass các đoạn test
- Đặt tên các test khó hiểu và tối nghĩa
- Không bắt đầu từ các test đơn giản nhất.
- Chỉ chạy lại các test đang bị thất bại
- Quên chạy tất cả các test một cách thường xuyên
- Viết một test với kịch bản quá phức tạp, hoặc viết quá nhiều test cùng một lúc
Còn đối với đội/nhóm phát triển phần mềm, cũng cần tránh một số lỗi như:
- Áp dụng từng phần: chỉ một số nhỏ các thành viên trong nhóm sử dụng TDD.
- Ít cập nhật các test: Thường dẫn đến việc các test không còn đúng với logic, và khi đó test không còn tác dụng.
- Các test bị bỏ rơi ( tức là hiếm khi hoặc không bao giờ chạy các test này), đôi khi là do mã nguồn của test không được cập nhật, hoặc do việc thay đổi của nhóm.
6. TDD trong Agile
TDD đáp ứng "Tuyên ngôn về Agile" khi bản thân quy trình TDD thúc đẩy tính thực tiễn của sản phẩm, tương tác với người dùng. Để phát huy tối đa những lợi ích mà TDD mang lại, độ lớn của 1 đơn vị tính năng phần mềm (unit of function) cần đủ nhỏ để kịch bản kiểm thử dễ dàng được xây dựng và đọc hiểu, thời gian debug các kịch bản kiểm thử(test-case) khi chạy thất bại cũng giảm thiểu hơn.
Thực tế cho thấy một số sự kết hợp giữa TDD và mô hình Agile khác như Scrum có thể hỗ trợ và tối ưu lợi ích của nhau. Ví dụ, việc chia nhỏ Backlog thành các User Story của Scrum khiến việc xây dựng kịch bản kiểm thử hướng TDD trở nên dễ dàng và thuận tiện. Thêm vào đó, cả Scrum và TDD tương đồng trong việc loại bỏ sự chuyên hóa về vai trò của bộ đôi Developer – Tester. Vì lý do đó, đôi lúc có thể bạn sẽ thấy cả TDD và Scrum được áp dụng trong cùng 1 dự án.
6. Tổng kết
- TDD là viết tắt của Test-driven development.
- TDD có nghĩa là thực hiện chỉnh sửa code để pass một test-case đã được thiết kế trước đó
- Nó tập trung vào việc code hơn là thiết kế các test-case
- Trong Kỹ thuật phần mềm, đôi khi nó còn được gọi là "Test First Development"
- Kiểm thử TDD bao gồm việc refactor các đoạn code như việc thêm mới/ thay đổi code đã có mà không ảnh hưởng đến hoạt động của nó
- Khi sử dụng TDD, code trở nên rõ ràng, đơn giản và dễ hiểu hơn.