Tìm hiểu mô hình TDD (Test - Driven Development) và cách áp dụng

Lập trình TDD, có nên hay không?

Posted by Box XV on February 1, 2023. 11 min read.

Automation Testing

Gần đây, mô hình phát triển TDD (Test Driven Development) đang trở nên hot, được áp dụng nhiều. Mô hình này dựa trên khái niệm: Với mỗi chức năng, ta viết Unit Test trước, sau đó viết hàm hiện thực chức năng để unit test pass. Một số công ty ở Việt Nam cũng đang áp dụng mô hình này, trong khi phỏng vấn xin việc cũng có.

I. Unit testing

Unit Testing

Tại sao bắt đầu viết unit test lại khó đến vậy?

Cách đây khoảng 6 năm, tôi muốn viết unit test để tự động hóa việc kiểm thử ứng dụng mà tôi đang tham gia xây dựng. Đó là những gì một nhà phát triển giỏi làm và tôi mong muốn trở thành một nhà phát triển giỏi.

Nhưng bằng cách nào?

Ứng dụng mà tôi tham gia xây dựng không có bất kỳ bài kiểm tra unit test nào để tự động hóa việc kiểm thử. Chúng tôi phải kiểm tra các chức năng hoàn toàn bằng cơm, đây là một công việc lặp đi lặp lại rất nhàm chán và bỏ sót nhiều bug (QC rất thích điều này, còn tôi thì không).

Tôi muốn thay đổi điều này và đã tìm thấy giải pháp là sử dụng unit test sau khi lang thang trên Google. Tôi đã thử làm theo ví dụ trong các hướng dẫn này và các ví dụ này chạy ngon lành. Tôi nghĩ rằng mình đã tìm thấy kim chỉ nam và suýt nữa đã làm như Acsimet đã từng làm cách đây hơn 2000 năm.

Tuy nhiên khi tôi muốn viết unit test cho dự án thực tế của tôi thì không áp dụng được. Các ví dụ mà tôi làm theo thường rất đơn giản, chẳng hạn như một chương trình cho phép cộng, trừ, nhân, chia hai số nguyên.

Trong khi ứng dụng mà tôi muốn viết unit test khá phức tạp: có giao diện Frontend và Backend, có các API xử lý business ở đằng sau và hầu hết thực hiện các thao tác CRUD (Create, Read, Update, Delete) với cơ sở dữ liệu.

Vấn đề ở đây là tôi cần viết unit test cho cái gì? Vì ứng dụng có rất nhiều thành phần khác nhau, từ UI đến API, business logic tới repository, database, …

Một vấn đề khác là mọi người luôn nói, “các bài kiểm tra unit test là quan trọng. Bạn phải làm chúng!” và sau đó không bao giờ nói cho tôi biết làm thế nào để viết chúng và làm thế nào để viết chúng thật tốt.

Loạt bài này trình bày một số thứ tôi đã học được và sử dụng trong công việc của tôi. Tôi đã học được rất nhiều về kiểm thử và đặc biệt là kiểm thử đơn vị tự động, và tôi hy vọng rằng một số điều tôi học được sẽ hữu ích cho độc giả của tôi.

Có nên viết unit test cho mọi thứ không?

Câu trả lời ngắn gọn là KHÔNG

Không phải tất cả mọi thứ đều cần phải được kiểm tra bằng unit test. Các đoạn mã có thể không bao giờ thay đổi, hoặc không quan trọng đối với chức năng chính của hệ thống, hoặc rất khó để viết unit test hoặc đơn giản là quá lãng phí khi viết unit test, …

Ngoài ra, chính xác khi nào và tại sao một thứ gì đó cần được viết unit test. Không có một kế hoạch kiểm thử nào phù hợp với tất cả. Mọi hệ thống đều khác nhau, mọi nhu cầu đều khác nhau, và bạn phải đánh giá phạm vi kiểm thử cần thiết dựa trên thiết kế và độ phức tạp của hệ thống bạn đang xây dựng, chưa kể thời gian thực hiện các kiểm tra đó.

Vậy chúng ta nên viết unit test cho cái gì?

Chúng ta nên viết unit test cho bất cứ điều gì là:

  • Quan trọng đối với toàn bộ chức năng của ứng dụng.
  • Đã biết hoặc có khả năng hỏng hóc cao.
  • Có khả năng thay đổi trong tương lai.

Nói cách khác: bạn nên kiểm tra mã nào quan trọng, dễ hỏng hoặc có khả năng thay đổi.

Nói một cách đơn giản và dễ hiểu nhất thì nên viết unit test cho:

  • Các lớp thư viện, lớp hạ tầng dùng chung cho nhiều project.
  • Các lớp tầng business logic.
  • Các lớp controller của Web UI và Web API.

II. Unit testing C#

Làm thế nào để chúng tôi viết unit test trong C#?

Tôi rất vui vì bạn đã hỏi điều đó. Chính xác thì chúng ta phải viết unit test như thế nào?

Có rất nhiều thư viện cho unit test có sẵn trong thế giới .NET như MSTest, NUnit, XUnit và các thư viện khác.

  • msTest: mature, slow. Là khung kiểm thử của Microsoft cho tất cả các ngôn ngữ .NET. Nó có thể mở rộng và hoạt động với cả .NET CLI và Visual Studio.
  • NUnit: mature, feature packed, fast. Là một khuôn khổ kiểm tra đơn vị cho tất cả các ngôn ngữ .NET. Ban đầu được chuyển từ JUnit, bản phát hành sản xuất hiện tại đã được viết lại với nhiều tính năng mới và hỗ trợ cho một loạt các nền tảng .NET. NUnit cũng là một dự án của .NET Foundation.
  • xUnit.net: new, fast. Là một công cụ kiểm thử đơn vị miễn phí, mã nguồn mở, tập trung vào cộng đồng cho .NET. Được viết bởi nhà phát triển ban đầu NUnit v2, xUnit.net là công nghệ mới nhất để kiểm tra đơn vị các ứng dụng .NET. xUnit.net hoạt động với ReSharper, CodeRush, TestDriven.NET và Xamarin. Đây là một dự án thuộc .NET Foundation và hoạt động theo quy tắc ứng xử của .NET Foundation.

Đây là những thứ phổ biến nhất và chúng hoạt động theo những cách tương tự nhau. Tuy nhiên, theo ý kiến ​​của tôi xUnit mang lại sự dễ sử dụng và khả năng tái sử dụng tốt nhất.

Unit Testing

Bảng Cheat-Sheet tham khảo cho Unit Test dễ dàng hơn

1. Unit Test trong C# với MSTest

Unit Testing

Unit Testing

Walkthrough: Create and run unit tests for managed code
Phần 1: Cách viết Unit Test trong C#
Phần 2: Sử dụng Unit Test để cải thiện code

2. Unit Test trong C# với NUnit

Trước đây, để viết Unit Test trong C#, ta thường phải tạo một project test riêng, sử dụng thư viện MSTest của Microsoft. MSTest hỗ trợ khá nhiều chức năng: Test dữ liệu từ database, đo performance hệ thống, xuất dữ liệu report. Tuy nhiên, do MSTest đi kèm với Visual Studio, không thể chạy riêng rẽ, lại khá nặng nề, do đó NUnit được nhiều người ưa thích hơn. NUnit có 1 bộ runner riêng, có thể chạy UnitTest độc lập không cần VisualStudio, ngoài ra nó cũng hỗ trợ một số tính năng mà MSTest không có (parameter test, Assert Throw).

[TUTORIAL] VIẾT UNIT TEST TRONG C# VỚI NUNIT

3. Unit Test trong C# với xUnit

XUnit là một framework unit test cho .NET cung cấp một cách dễ dàng để viết code, chạy và debug các bài kiểm tra unit test.

Trong xUnit, một bài kiểm tra cơ bản là một phương thức trong lớp công khai, không có tham số hoặc giá trị trả về, được gắn nhãn bằng thuộc tính Fact, như sau:

public class TestCases
{
    [Fact]
    public void ClassName_MethodName_ExpectedResult() 
    {
        // Arrange
        
        // Fact
        
        // Assert
    }
}

Tại thời điểm này, bài kiểm tra trên chính xác là không có gì. Để điền vào những gì bài kiểm tra nên làm, chúng ta có thể làm theo phương pháp “Arrange, Act, Assert.”

LƯU Ý: Bạn có thể thiết lập xUnit để chạy các bài kiểm tra với đầu vào và đầu ra bằng thuộc tínhTheory], hãy xem bài viết “Sử dụng xUnit để kiểm tra mã C# của bạn” để biết một ví dụ tuyệt vời. Chúng ta sẽ tìm hiểu kỹ hơn về vấn đề này trong phần sau của loạt bài này.

Arrange, Act, Assert (AAA) Cụm từ “Arrange, Act, Assert” hay AAA là một cách hay để ghi nhớ cấu trúc của một bài kiểm tra unit test.

  • Arrange: có nghĩa là để thiết lập môi trường thử nghiệm cần thiết và các phụ thuộc. Điều này có thể có nghĩa là tạo một tập hợp dữ liệu thử nghiệm tốt đã biết hoặc tạo mock của một phần phụ thuộc mà chúng ta không muốn kiểm tra. Tóm lại, “Arrange” có nghĩa là “tạo ra những gì bài kiểm tra cần để chạy.”
  • Act: có nghĩa là thực thi mã cần được kiểm tra, đã được thiết lập trong bước Arrange.
  • Assert: có nghĩa là kiểm tra kết quả và đầu ra và xác nhận rằng chúng là những gì chúng ta mong đợi.

Ví dụ dưới đây là một bài kiểm tra unit test có sử dụng mock:

[Fact]
public void PlayerService_GetAllPlayers_InvalidLeague()
{
    //Arrange
    var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);

    var playerService = new PlayerService(new MockPlayerRepository().Object,
        new MockTeamRepository().Object,
        mockLeagueRepo.Object);

    //Act
    var allPlayers = playerService.GetForLeague(1);

    //Assert
    Assert.Empty(allPlayers);
    mockLeagueRepo.VerifyIsValid(Times.Once());
}

Đừng lo lắng về đoạn mã này; bây giờ, tất cả những gì bạn cần nhớ là mô hình “Arrange, Act, Assert”. Chúng ta sẽ xem xét chính xác đoạn mã này làm gì trong phần tiếp theo của loạt bài này.

Ở phần tiếp theo, chúng tôi sẽ trình bày cách sử dụng “mock” cho phép chúng ta dễ dàng tạo và thiết lập các lớp giả lập để thử nghiệm (cũng như tìm hiểu chính xác “mock” là gì).

Sử dụng Moq để viết unit test trong ASP.NET Core TDD VỚI XUNIT TRÊN DỰ ÁN .NET

Unit Testing

III. Mô hình TDD (Test - Driven Development)

Phát triển hướng kiểm thử TDD (Test-Driven Development) là một phương pháp tiếp cận cải tiến để phát triển phần mềm trong đó kết hợp phương pháp Phát triển kiểm thử trước (Test First Development) và phương pháp Điều chỉnh lại mã nguồn (Refactoring).

Mục tiêu quan trọng nhất của TDD là hãy nghĩ về thiết kế của bạn 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.

1.TDD là gì?

TDD (Test Driven Development) là một phương thức làm việc, hay một quy trình viết mã hiện đại. Lập trình viên sẽ thực hiện thông qua các bước nhỏ (BabyStep) và tiến độ được đảm bảo liên tục bằng cách viết và chạy các bài test tự động (automated tests). Quá trình lập trình trong TDD cực kỳ chú trọng vào các bước liên tục sau:

  1. Viết 1 test cho hàm mới. Đảm bảo rằng test sẽ fail.
  2. Chuyển qua viết code sơ khai nhất cho hàm đó để test có thể pass.
  3. Tối ưu hóa đoạn code của hàm vừa viết sao cho đảm bảo test vẫn pass và tối ưu nhất cho việc lập trình kế tiếp
  4. Lặp lại cho các hàm khác từ bước 1

Thực tế, nên sử dụng UnitTestFramework cho TDD (như JUnit trong Java), chúng ta có thể có được môi trường hiệu quả vì các test được thông báo rõ rang thông qua màu sắc:

  • Đỏ: test fail, chuyển sang viết function cho test pass
  • Xanh lá: viết một test mới hoặc tối ưu code đã viết trong màu đỏ.

2. 3 điều luật khi áp dụng TDD

  1. Không cho phép viết bất kỳ một mã chương trình nào cho tới khi nó làm một test bị fail trở nên pass.
  2. Không cho phép viết nhiều hơn một unit test mà nếu chỉ cần 1 unit test cung đã đủ để fail. Hãy chuyển sang viết code function để pass test đó trước.
  3. Không cho phép viết nhiều hơn 1 mã chương trình mà nó đã đủ làm một test bị fail chuyển sang pass.

3. Mô hình chu trình TDD

4. Các cấp độ TDD

  1. Mức chấp nhận (Acceptance TDD (ATDD)): với ATDD thì bạn viết một test chấp nhận đơn (single acceptance test) hoặc một đặc tả hành vi (behavioral specification) tùy theo cách gọi của bạn; mà test đó chỉ cần đủ cho các mã chường trình sản phẩm thực hiện (pass or fail) được test đó. Acceptance TDD còn được gọi là Behavior Driven Development (BDD).
  2. Mức lập trình (Developer TDD): với mức này bạn cần viết một test lập trình đơn (single developer test) đôi khi được gọi là unit test mà test đó chỉ cần đủ cho các mã chường trình sản phẩm thực hiện (pass or fail) được test đó. Developer TDD thông thường được gọi là TDD.

5. Các lỗi thường gặp khi áp dụng TDD

  • Không quan tâm đến các test bị fail
  • Quên đi thao tác tối ưu sau khi viết code cho test pass
  • Thực hiện tối ưu code trong lúc viết code cho test pass => không nên như vậy
  • Đặ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 và không theo các baby step.
  • Chỉ chạy mỗi test đang bị fail hiện tại
  • Viết một test với kịch bản quá phức tạp

6. Các ví dụ tham khảo về TDD

  • Ngắn:
    • Chapter 6 – Agile principles, patterns, and practices in C# – by Martin C. Robert, Martin Micah. Khá thú vị. Xem online tại đây.
    • Phần 3, 4, 5 của Craftsman.
  • Trung bình:
    • Part I – Test-Driven Development by example – Kent Beck.
    • Part III – Test-Driven Development: A practical guide – David Astels.
    • Phần 6, 7, 8, 9, 10 của Craftsman.
  • Dài:
    • Part II – Test-Driven Development in Microsoft .NET – James W. Newkirk, Alexei A. Vorontsov.
    • Part III – Growing object-oriented software, guided by test – Steve Freeman, Nat Pryce.

7. Các công cụ hỗ trợ

Các công cụ phục vụ cho TDD, thường là các nền tảng cho kiểm thử mã nguồn mức đơn vị (unit test):


Tham khảo: