Test-Driven Development căn bản

Hầu hết lập trình viên từng nghe khái niệm Test-Driven Development (TDD). Tuy nhiên, rất ít người áp dụng nó vào dự án vì họ ngán phải thực hiện thêm nhiều tao tác khi viết code.

Thực ra, dù không dùng TDD, ta cũng có sử dụng test. Thông thường, sau khi viết xong một đoạn code, ta chạy thử và thay đổi vài dữ kiện để kiểm tra kết quả bằng mắt. Đó là dạng test thủ công và nó không đạt hiệu quả cao. Với TDD, ta có thể tự động hóa quá trình này với một cú click.

Những người chưa bao giờ sử dụng TDD thường nghĩ TDD sẽ tạo thêm gánh nặng cho việc viết code. Khâu test thường là khâu nhàm chán, mệt mỏi và tốn nhiều thời gian. Giờ phải thêm một quy trình test mới thì chắc chắn khiến nhiều người thấy nản. Tuy nhiên, TDD rất gọn nhẹ, chỉ mất khoảng vài chục giây để viết một test.

Điều thú vị của TDD là nó được tích hợp vào quy trình viết code, nghĩa là ta sẽ nhảy qua lại giữa test và code. Khi hoàn tất ứng dụng, ta cũng xong luôn khâu TDD. Việc nhảy qua lại giữa test và code diễn ra rất nhanh. Do đó, nó không ảnh hưởng đáng kể đến thời gian viết code.

Tổng quan về Unit test

Unit test giúp xác thực code để đảm bảo rằng chúng vẫn hoạt động theo những tiêu chí đã đặt ra. Như tên gọi của nó, Unit test chỉ test một đơn vị (unit) nhỏ của code, thường là một method. Toàn bộ quy trình này có thể thực hiện thông qua test framework. Trong bài viết này, tôi sẽ dùng MSTest, một test framework được cung cấp sẵn trong Visual Studio 2013.

Lưu ý rằng MSTest là một thành phần của Visual Studio chứ không phải của .NET Framework. Do đó, để sử dụng test framework này, bắt buộc ta phải cài Visual Studio.

Unit test chỉ là code bình thường mà ta đã quen thuộc. Tuy nhiên, để Visual Studio nhận biết đây là test, ta phải thông báo cho nó bằng attribute nằm trên tên class và method.

1 [TestClass]
2 public class UnitTest1
3 {
4     [TestMethod]
5     public void TestMethod1()
6     {
7         // Test code
8     }
9 }

Trong ví dụ trên, ta thấy test method trả về kiểu void, không nhận tham số và phải public.

Để sử dụng MSTest, ta thêm một Test project vào Solution. Để làm việc này, click phải vào tên Solution và chọn Add > New Project, sau đó chọn Test ở danh sách bên trái và chọn tiếp Unit Test Project ở bên phải. Lúc này, Visual Studio sẽ tạo sẵn test class UnitTest1 chứa test method TestMethod1.

Tiếp theo, ta cần thêm một reference từ project Test đến project muốn test. Click phải vào tên project Test và chọn Add Reference, sau đó chọn tiếp tên project muốn test. Bây giờ, ta có thể viết code trong test method.

Giả sử tôi có một class cần test như sau:

 1 class Person
 2 {
 3     public int Age { get; set; }
 4 
 5     public string Message()
 6     {
 7         string message;
 8         if (Age < 13)
 9             message = "You're a kid";
10         else if (Age < 20)
11             message = "You're a teenager";
12         else
13             message = "You're an adult";
14         return message;
15     }
16 }

Tôi sẽ viết test cho method trong class này. Để viết test method, ta theo quy trình 3A:

Arrange
Đây là khâu chuẩn bị các đối tượng có liên quan để test. Thông thường, ta tạo các biến để chứa đối tượng hoặc chứa kết quả test.
Act
Đây là khâu thực thi các thuật toán để cho ra kết quả muốn test. Ta chạy các method và lưu trữ kết quả vào một biến để dùng cho khâu Assert kế tiếp.
Assert
Đây là khâu cuối cùng. Ở khâu này, ta dùng một loạt các câu lệnh Assert để so sánh, đánh giá kết quả trả về từ khâu Act với kết quả ta mong đợi. Các câu lệnh Assert được cung cấp bởi test framework. Mỗi framework sẽ có một tập các lệnh Assert khác nhau. Do đó, bạn nên tham khảo tài liệu hướng dẫn của framework trước khi bắt tay vào viết test.

Để viết test method cho class Person ở trên, tôi tiến hành như sau:

 1 [TestClass]
 2 public class PersonTest
 3 {
 4     [TestMethod]
 5     public void MessageAdultTest()
 6     {
 7         // Arrange
 8         Person person = new Person() { Age = 24 };
 9         string expected = "You're an adult";
10 
11         // Act
12         string message = person.Message();
13 
14         // Assert
15         Assert.AreEqual(expected, message);
16     }
17 }

Để chạy test, click phải vào test method và chọn Run Tests. Khi chạy xong, Visual Studio hiển thị kết quả test trong khung cửa sổ Test Explorer. Test thành công (Passed) được đánh dấu bởi vòng tròn xanh chứa dấu check, còn test thất bại (Failed) có màu đỏ với dấu X bên trong.

Nhìn lại code ở trên, ta thấy lệnh if chia đường đi của code làm 3 nhánh: nhánh đầu tiên khi Age < 13, nhánh thứ hai khi Age >= 13Age < 20, nhánh cuối cùng khi Age >= 20. Tuy nhiên, trong test method tôi chỉ test một nhánh khi Age >= 20. Điều này nghĩa là độ bao phủ code (code coverage) của test chưa hiệu quả do vẫn còn những điều kiện chưa test. Ở đây, ta nên tạo thêm 2 test method để test cả 2 nhánh còn lại.

Trước khi qua phần sau, tôi muốn báo cho bạn một tin sốc: những gì tôi hướng dẫn không phải là Test-Driven Development, nó chỉ là Unit Test mà thôi. Có sử dụng test không có nghĩa là ta đang dùng TDD. Để được gọi là TDD, ta buộc phải viết test trước khi viết code.

Quy trình của TDD

Quy trình Test-Driven Development trông quái lạ vì nó rất khác với những gì hầu hết mọi người quen làm. Nó quy định rằng ta phải làm cho test thất bại (fail), sau đó làm cho thành công (pass). Cuối cùng, ta sẽ refactor sao cho test vẫn pass. Khi test thất bại, ta thấy biểu tượng màu đỏ. Khi test pass, ta thấy biểu tượng màu lục. Để giúp ghi nhớ quy trình TDD, ta có câu thần chú sau:

  • Đỏ (Fail)
  • Xanh (Pass)
  • Refactor

Hai bước đầu tiên là để đảm báo tính xác thực của test vì ta hoàn toàn dựa vào chúng để xem code có hoạt động tốt hay không. Nếu test cho kết quả không tin cậy, việc dùng test trở nên vô nghĩa. Do đó, làm cho test fail, rồi làm cho pass là khâu kiểm tra chất lượng để chắc rằng test cho kết quả chuẩn xác.

Trong phần trước, tôi đã nói đến một vấn đề quan trọng trong TDD, đó là phải viết test trước. Nếu không viết test trước thì đó không phải là TDD mặc dù có dùng test. Những người không quen với quy trình TDD đều thắc mắc: nếu viết test trước thì test cái gì? Đã viết code nào đâu mà test? Đúng là ta vẫn chưa viết một dòng code nào, nhưng cái ta có là ý định sử dụng code ra sao. Do vậy, khi viết test, ta sẽ viết ý định sử dụng code vào test method.

Giả sử tôi có ý định tạo một class Number. Trong class này, tôi muốn có một property kiểu int tên Total và một method tên Add nhận một tham số tên i có kiểu int. Method này sẽ thêm số i vào property Total. Với ý định như thế, tôi sẽ tiến hành viết test như sau:

 1 [TestClass]
 2 public class NumberTest
 3 {
 4     [TestMethod]
 5     public void AddTest()
 6     {
 7         // Arrange
 8         Number number = new Number() { Total = 20 };
 9 
10         // Act
11         number.Add(5);
12 
13         // Assert
14         Assert.AreEqual(25, number.Total);
15     }
16 }

Khi viết đoạn code trên, IntelliSense cố gợi ý cho tôi trong lúc đang gõ code. Vì class tôi dùng không tồn tại nên tôi làm ngơ IntelliSense và tiếp tục hoàn tất đoạn test.

Sau khi viết test xong, tôi nhảy sang phần code và bắt đầu viết. Tuy nhiên, lúc này tôi chỉ cung cấp cái vỏ ngoài thôi chứ không viết chi tiết bên trong thân method. Mục đích của tôi là làm cho test fail.

1 public class Number
2 {
3     public int Total { get; set; }
4     public void Add(int i)
5     {
6         // Nothing
7     }
8 }

Bây giờ, tôi nhảy qua phần test và chạy nó. Cửa sổ Test Explorer hiện ra và thông báo test đã fail đúng như mong đợi. Tiếp theo, tôi làm cho test pass:

1 public class Number
2 {
3     public int Total { get; set; }
4     public void Add(int i)
5     {
6         Total = 25;
7     }
8 }

Như đã nói trên, sau khi làm test fail, tôi làm cho nó pass. Lúc này, tôi không quan tâm đến việc nó có hợp logic hay không, mục đích của khâu này là làm cho test pass mà thôi. Giá trị 25 giúp tôi thực hiện việc này dễ dàng nhất.

Sau đó, tôi nhảy qua phần test và chạy. Kết quả pass hiện ra trong Test Explorer. Tiếp theo, tôi chuyển sang bước cuối cùng, đó là refactor code.

1 public class Number
2 {
3     public int Total { get; set; }
4     public void Add(int i)
5     {
6         Total += i;
7     }
8 }

Tôi chạy test và kết quả vẫn pass. Đây chính là điều tôi mong đợi.

Lời kết

Như bạn thấy, TDD có một quy trình làm việc hơi lạ. Để thành thạo, bạn cần phải luyện tập nhiều. Từ dự án tiếp theo, bạn nên áp dụng TDD để nó trở thành một thói quen. Bạn cũng không nên bỏ qua bước làm quen với test framework. Trước khi bắt đầu, bạn nên xem qua documentation của test framework để nắm những cú pháp quan trọng.