Chuyển đến nội dung chính

Test Coverage trong .NET: Tổng hợp Coverage từ nhiều Test Project - Day 3

Ở Day 1, chúng ta đã tạo Unit Test và làm quen với Code Coverage. Ở Day 2, chúng ta tìm hiểu sự khác biệt giữa Line Coverage, Branch Coverage và cách viết test case có ý nghĩa.

Trong dự án thực tế, solution thường có nhiều test project. Ví dụ:

  • LibraryManagement.Tests: chứa Unit Test để kiểm tra logic nghiệp vụ.
  • LibraryManagement.IntegrationTests: chứa Integration Test để kiểm tra sự phối hợp giữa các thành phần.

Khi chạy từng test project, chúng ta có thể thu được nhiều file kết quả Coverage khác nhau. Nếu muốn xem tổng quan mức độ bao phủ của ứng dụng, chúng ta cần biết cách thu thập, phân biệt và tổng hợp các file kết quả này.

Trong bài viết này, chúng ta sẽ sử dụng Coverlet để thu thập Coverage và ReportGenerator để tạo báo cáo HTML dễ đọc.

1. Mục tiêu bài học

Sau bài này, bạn sẽ có thể:

  • Hiểu vai trò của nhiều test project trong một solution .NET.
  • Thu thập Coverage riêng cho Unit Test và Integration Test.
  • Giải thích được tại sao thư mục TestResults có nhiều file Coverage.
  • Dùng ReportGenerator tạo báo cáo cho từng test project và báo cáo tổng hợp.
  • Đọc báo cáo tổng thể mà không nhầm lẫn giữa kết quả của các lần chạy khác nhau.

2. Solution có nhiều Test Project

2.1. Cấu trúc solution minh họa

Giả sử solution LibraryManagement có cấu trúc sau:

LibraryManagement/
├── LibraryManagement.sln
├── src/
│   └── LibraryManagement/
│       └── LibraryManagement.csproj
├── tests/
│   ├── LibraryManagement.Tests/
│   │   └── LibraryManagement.Tests.csproj
│   └── LibraryManagement.IntegrationTests/
│       └── LibraryManagement.IntegrationTests.csproj
└── artifacts/
    └── coverage/

Trong đó:

Project Vai trò
LibraryManagement Production code, chứa logic nghiệp vụ như LibraryService.
LibraryManagement.Tests Unit Test, tập trung kiểm tra từng đơn vị logic với các dependency được kiểm soát.
LibraryManagement.IntegrationTests Integration Test, kiểm tra sự phối hợp giữa nhiều thành phần, chẳng hạn service với database hoặc API với các thành phần phụ thuộc.

Đây là cấu trúc minh họa. Tên thư mục và cách tổ chức project có thể khác nhau tùy solution.

2.2. Unit Test và Integration Test khác nhau thế nào?

Hãy tiếp tục sử dụng phương thức CalculateLateFee đã xuất hiện trong các bài trước:

public decimal CalculateLateFee(int overdueDays)
{
    if (overdueDays < 0)
    {
        throw new ArgumentException(
            "Overdue days cannot be negative.");
    }

    if (overdueDays == 0)
    {
        return 0m;
    }

    return overdueDays * 5000m;
}

Unit Test có thể kiểm tra trực tiếp các trường hợp:

  • overdueDays = -1: ném ArgumentException.
  • overdueDays = 0: trả về 0.
  • overdueDays = 1: trả về 5000.

Trong khi đó, Integration Test có thể kiểm tra một luồng nghiệp vụ rộng hơn, ví dụ một yêu cầu tính phí đi qua API, service và database.

Điều quan trọng: Coverage được thu thập trong một lần chạy test cho biết code nào đã được thực thi trong lần chạy đó. Nó không tự cho biết test nào là tốt, và cũng không chứng minh rằng mọi hành vi nghiệp vụ đã được kiểm tra đầy đủ.

3. Coverlet thu thập Coverage như thế nào?

Coverlet là công cụ thu thập Code Coverage phổ biến trong hệ sinh thái .NET. Khi sử dụng tích hợp VSTest thông qua package coverlet.collector, chúng ta có thể chạy:

dotnet test --collect:"XPlat Code Coverage"

Tham số --collect:"XPlat Code Coverage" yêu cầu test runner thu thập dữ liệu Coverage bằng Coverlet Data Collector.

Với cách tích hợp này, file kết quả mặc định thường có tên:

coverage.cobertura.xml

File XML chứa dữ liệu Coverage ở định dạng Cobertura. Chúng ta có thể dùng nó làm đầu vào cho ReportGenerator.

3.1. Kiểm tra package cần thiết

Trong từng test project sử dụng cách tích hợp trên, hãy kiểm tra package coverlet.collector đã được cài đặt hay chưa.

Nếu chưa có, chạy các lệnh sau từ thư mục solution:

dotnet add .\tests\LibraryManagement.Tests\LibraryManagement.Tests.csproj package coverlet.collector

dotnet add .\tests\LibraryManagement.IntegrationTests\LibraryManagement.IntegrationTests.csproj package coverlet.collector

Nếu project đã có package này thì không cần cài lại. Ngoài ra, hãy kiểm tra project sử dụng test runner tương thích với cách tích hợp Coverlet đang chọn.

4. Thu thập Coverage cho từng Test Project

Trước khi chạy, hãy mở terminal tại thư mục chứa LibraryManagement.sln.

Trong ví dụ này, chúng ta đặt kết quả thô vào các thư mục riêng để dễ phân biệt và tránh vô tình sử dụng dữ liệu cũ.

4.1. Chuẩn bị thư mục kết quả

Với PowerShell, có thể tạo các thư mục sau:

New-Item -ItemType Directory -Force `
    -Path .\artifacts\coverage\raw\unit | Out-Null

New-Item -ItemType Directory -Force `
    -Path .\artifacts\coverage\raw\integration | Out-Null

Thư mục raw sẽ chứa kết quả Coverage ban đầu. Chúng ta chưa tạo báo cáo HTML ở bước này.

4.2. Chạy Unit Test và thu thập Coverage

dotnet test `
    .\tests\LibraryManagement.Tests\LibraryManagement.Tests.csproj `
    --collect:"XPlat Code Coverage" `
    --results-directory .\artifacts\coverage\raw\unit

Lệnh này chỉ chạy project LibraryManagement.Tests. Các file kết quả của lần chạy được đặt dưới thư mục đã chỉ định, thường nằm trong một thư mục con có tên GUID.

4.3. Chạy Integration Test và thu thập Coverage

dotnet test `
    .\tests\LibraryManagement.IntegrationTests\LibraryManagement.IntegrationTests.csproj `
    --collect:"XPlat Code Coverage" `
    --results-directory .\artifacts\coverage\raw\integration

Lệnh này chạy riêng LibraryManagement.IntegrationTests và lưu kết quả vào thư mục khác với Unit Test.

Nếu Integration Test cần database hoặc dịch vụ phụ thuộc, bạn phải chuẩn bị môi trường tương ứng trước khi chạy.

4.4. Hình dung luồng xử lý

flowchart TD
    A["LibraryManagement.sln"]
    A --> B["LibraryManagement.Tests"]
    A --> C["LibraryManagement.IntegrationTests"]
    B --> D["Coverlet"]
    C --> E["Coverlet"]
    D --> F["coverage.cobertura.xml - Unit"]
    E --> G["coverage.cobertura.xml - Integration"]
    F --> H["ReportGenerator"]
    G --> H
    H --> I["Báo cáo HTML tổng hợp"]

Điểm cần nhớ là chúng ta có thể thu thập Coverage riêng cho mỗi test project, sau đó chọn báo cáo nào cần tạo từ các file kết quả đã thu thập.

5. Tại sao TestResults có nhiều file Coverage?

Khi chạy lệnh Coverage nhiều lần, bạn có thể thấy cấu trúc tương tự như sau:

TestResults/
├── 1a2b3c4d-1111-2222-3333-444444444444/
│   └── coverage.cobertura.xml
├── 5e6f7a8b-1111-2222-3333-444444444444/
│   └── coverage.cobertura.xml
└── 9c0d1e2f-1111-2222-3333-444444444444/
    └── coverage.cobertura.xml

Đây không nhất thiết là lỗi hay ba báo cáo cho ba project khác nhau.

Với VSTest, mỗi lần chạy thường tạo một thư mục kết quả riêng có tên GUID. Vì vậy, nhiều lần chạy có thể tạo nhiều file cùng tên coverage.cobertura.xml.

5.1. Ba khái niệm cần phân biệt

Khái niệm Ý nghĩa
Test Project Project chứa các test case.
Test Run Một lần chạy test cụ thể.
Coverage Report Dữ liệu Coverage được tạo từ một lần chạy hoặc được tổng hợp từ nhiều báo cáo đầu vào.

Ví dụ, cùng một Unit Test project có thể tạo nhiều file Coverage sau nhiều lần chạy. Ngược lại, một báo cáo tổng hợp có thể sử dụng file Coverage từ nhiều test project khác nhau.

5.2. Tại sao không nên gom tất cả file XML một cách tùy tiện?

Giả sử bạn chạy test hôm nay, sau đó sửa code và chạy lại ngày mai. Nếu thư mục cũ vẫn còn, một lệnh tìm tất cả file coverage.cobertura.xml có thể vô tình lấy cả kết quả cũ.

Điều này khiến báo cáo tổng hợp không còn phản ánh chính xác lần kiểm thử mà bạn định đánh giá. Code từng được test ở lần chạy trước có thể xuất hiện là đã được bao phủ, dù lần chạy mới không thực thi code đó.

Nguyên tắc: Trước khi tổng hợp Coverage, hãy xác định rõ bạn muốn tổng hợp những lần chạy nào. Trong bài thực hành này, chúng ta sử dụng thư mục kết quả riêng cho từng lần chạy và chỉ tổng hợp các file nằm trong đó.

6. Cài đặt ReportGenerator

ReportGenerator chuyển các file Coverage như Cobertura XML thành báo cáo dễ đọc hơn, bao gồm báo cáo HTML có thể mở bằng trình duyệt.

Cài đặt công cụ dạng .NET Global Tool:

dotnet tool install -g dotnet-reportgenerator-globaltool

Nếu công cụ đã được cài đặt, bạn có thể cập nhật bằng:

dotnet tool update -g dotnet-reportgenerator-globaltool

Kiểm tra công cụ có thể chạy:

reportgenerator -help

Nếu PowerShell không tìm thấy lệnh reportgenerator, hãy kiểm tra thư mục cài đặt Global Tools của .NET có nằm trong biến môi trường PATH hay không.

7. Tạo báo cáo cho từng Test Project

Trước tiên, hãy tạo báo cáo riêng cho mỗi loại test. Cách này giúp bạn trả lời câu hỏi: “Chỉ riêng Unit Test đã thực thi được bao nhiêu code?” hoặc “Integration Test đã thực thi được những phần nào?”

7.1. Báo cáo Unit Test

reportgenerator `
    "-reports:artifacts/coverage/raw/unit/**/coverage.cobertura.xml" `
    "-targetdir:artifacts/coverage/report/unit" `
    "-reporttypes:Html"

Tham số:

  • -reports: đường dẫn đến các file Coverage đầu vào. Mẫu ** cho phép tìm file trong các thư mục con.
  • -targetdir: thư mục xuất báo cáo.
  • -reporttypes: định dạng báo cáo cần tạo; ở đây là HTML.

Sau khi lệnh chạy thành công, mở file:

artifacts/coverage/report/unit/index.html

7.2. Báo cáo Integration Test

reportgenerator `
    "-reports:artifacts/coverage/raw/integration/**/coverage.cobertura.xml" `
    "-targetdir:artifacts/coverage/report/integration" `
    "-reporttypes:Html"

Mở báo cáo tại:

artifacts/coverage/report/integration/index.html

Hai báo cáo này giúp phân tích riêng dữ liệu được tạo bởi hai nhóm test. Chúng không phải là báo cáo về tỷ lệ test case thành công; chúng thể hiện Coverage của code được thực thi trong các lần chạy tương ứng.

8. Tạo báo cáo Coverage tổng hợp

Sau khi đã có kết quả từ cả Unit Test và Integration Test, chúng ta có thể đưa nhiều file Coverage vào cùng một lần chạy ReportGenerator.

reportgenerator `
    "-reports:artifacts/coverage/raw/unit/**/coverage.cobertura.xml;artifacts/coverage/raw/integration/**/coverage.cobertura.xml" `
    "-targetdir:artifacts/coverage/report/all" `
    "-reporttypes:Html"

Ở đây, -reports chứa hai mẫu đường dẫn, được phân cách bằng dấu chấm phẩy. ReportGenerator sẽ đọc các file phù hợp và tạo báo cáo tổng hợp tại:

artifacts/coverage/report/all/index.html

8.1. Báo cáo tổng hợp có nghĩa là gì?

Giả sử Unit Test đã thực thi một số dòng trong LibraryService, còn Integration Test thực thi thêm các dòng khác thông qua một luồng nghiệp vụ rộng hơn.

Báo cáo tổng hợp giúp quan sát Coverage từ các nguồn kết quả đã chọn trong một báo cáo chung. Với cùng một module và cùng phiên bản code, những dòng được bao phủ bởi ít nhất một báo cáo đầu vào có thể được tính là đã bao phủ trong dữ liệu tổng hợp.

Ví dụ minh họa:

Nguồn test Dòng đã bao phủ Tổng dòng có thể đo
Unit Test 60 100
Integration Test 50 100
Kết hợp hai nhóm test 80 100

Số liệu trên chỉ là ví dụ minh họa; số dòng được bao phủ chung phụ thuộc vào những dòng mà hai nhóm test thực sự thực thi.

Không thể lấy 60 + 50 = 110 để kết luận đã bao phủ 110 dòng khác nhau. Một số dòng có thể được thực thi bởi cả hai nhóm test.

Tương tự, không nên cộng hoặc lấy trung bình đơn giản hai phần trăm Coverage để tính tỷ lệ tổng thể. Hãy để công cụ tổng hợp dữ liệu từ các báo cáo đầu vào.

8.2. Coverage tổng hợp không thay thế báo cáo riêng

Báo cáo tổng hợp trả lời câu hỏi: “Kết hợp các kết quả đã chọn, code nào đã được bao phủ?”

Báo cáo riêng trả lời câu hỏi: “Nhóm test này đã bao phủ những phần nào?”

Cả hai đều hữu ích. Khi điều tra một khu vực code chưa được kiểm thử, báo cáo riêng giúp bạn biết nên bổ sung Unit Test hay Integration Test. Báo cáo tổng hợp giúp xem xét mức độ bao phủ từ toàn bộ các nguồn test được chọn.

9. Cách đọc báo cáo HTML

Mở file index.html trong thư mục báo cáo bằng trình duyệt. Giao diện có thể khác đôi chút tùy phiên bản ReportGenerator và loại báo cáo được chọn.

9.1. Summary - Tổng quan

Trang tổng quan thường hiển thị các chỉ số Coverage, chẳng hạn:

  • Line Coverage: tỷ lệ dòng code có thể đo đã được thực thi.
  • Branch Coverage: tỷ lệ nhánh quyết định có thể đo đã được thực thi.
  • Covered / Total: số đơn vị được bao phủ so với tổng số đơn vị có thể đo.

Không phải tất cả phiên bản hoặc định dạng báo cáo đều hiển thị chính xác cùng một tập chỉ số. Hãy đọc tên chỉ số và đơn vị đi kèm thay vì chỉ nhìn vào phần trăm lớn nhất trên màn hình.

9.2. Assembly, Namespace và Class

Bạn có thể đi từ tổng quan xuống các thành phần chi tiết hơn, ví dụ assembly, namespace, class và file nguồn.

Khi thấy Coverage thấp, đừng chỉ dừng ở con số tổng. Hãy tìm xem class hoặc phương thức nào chưa được thực thi, rồi đối chiếu với các nhánh logic và yêu cầu nghiệp vụ.

9.3. Dòng code được bao phủ và chưa được bao phủ

Ở mức file nguồn, báo cáo có thể đánh dấu các dòng đã được thực thi và các dòng chưa được thực thi bằng màu sắc hoặc ký hiệu.

Với phương thức CalculateLateFee, nếu test chỉ gọi với overdueDays = 1, bạn có thể chưa kiểm tra nhánh ném exception hoặc nhánh trả về 0.

Hãy quay lại các bài trước để xem cách chọn test case cho giá trị âm, giá trị bằng 0 và giá trị dương. Coverage giúp phát hiện những phần code chưa được thực thi, nhưng bạn vẫn phải xác định kết quả đúng bằng assertion và quy tắc nghiệp vụ.

10. Các lỗi thường gặp

Lỗi 1: Không tìm thấy file coverage.cobertura.xml

Kiểm tra:

  • Test project có package coverlet.collector hay chưa.
  • Lệnh dotnet test có chạy thành công hay không.
  • Tham số --collect:"XPlat Code Coverage" đã được truyền đúng chưa.
  • File kết quả đang nằm ở thư mục TestResults mặc định hay thư mục đã chỉ định qua --results-directory.

Lỗi 2: ReportGenerator không tìm thấy báo cáo đầu vào

Kiểm tra mẫu đường dẫn trong -reports, đặc biệt tên thư mục, tên file và vị trí chạy lệnh. Nếu cần, hãy tìm trực tiếp file XML trong File Explorer trước khi chạy ReportGenerator.

Lỗi 3: Báo cáo tổng hợp có kết quả cao bất ngờ

Kiểm tra xem mẫu -reports có vô tình lấy file Coverage từ những lần chạy cũ hay không. Một báo cáo tổng hợp chỉ có ý nghĩa khi các file đầu vào thuộc đúng phạm vi và phiên bản code mà bạn muốn đánh giá.

Lỗi 4: Tổng Coverage không giống trung bình hai báo cáo riêng

Đây không nhất thiết là lỗi. Hai nhóm test có thể bao phủ trùng các dòng code, hoặc mỗi báo cáo có các module và tổng số dòng có thể đo khác nhau. Báo cáo tổng hợp phải được tính từ dữ liệu đầu vào, không phải bằng cách lấy trung bình các phần trăm riêng.

Lỗi 5: Coverage cao nhưng vẫn có bug

Một dòng code đã được thực thi không có nghĩa là mọi kết quả của dòng code đó đã được kiểm tra. Hãy kết hợp Coverage với assertion, boundary testing và việc kiểm tra các nhánh logic quan trọng.

11. Bài tập thực hành

Bài tập 1: Thu thập Coverage riêng

  1. Chạy LibraryManagement.Tests với Coverlet.
  2. Chạy LibraryManagement.IntegrationTests với Coverlet.
  3. Tìm file coverage.cobertura.xml của từng nhóm test.
  4. Ghi lại đường dẫn thực tế của các file kết quả.

Câu hỏi: Vì sao hai lần chạy có thể tạo hai thư mục kết quả khác nhau dù cùng sử dụng tên file coverage.cobertura.xml?

Bài tập 2: Tạo ba báo cáo

  1. Tạo báo cáo HTML chỉ dành cho Unit Test.
  2. Tạo báo cáo HTML chỉ dành cho Integration Test.
  3. Tạo báo cáo HTML tổng hợp từ cả hai nhóm.

Câu hỏi: Báo cáo nào giúp bạn biết riêng Unit Test đã bao phủ phần code nào? Báo cáo nào cho thấy phần code được bao phủ khi kết hợp cả hai nhóm test?

Bài tập 3: Điều tra code chưa được bao phủ

Mở báo cáo tổng hợp và tìm một class hoặc phương thức có Coverage thấp hơn mong đợi.

  • Phương thức đó có nhánh nào chưa được thực thi?
  • Có cần thêm Unit Test cho trường hợp đó không?
  • Nếu nhánh đó chỉ xuất hiện khi nhiều thành phần phối hợp, Integration Test có phù hợp không?

Không cần cố đạt 100% ngay lập tức. Mục tiêu là học cách dùng báo cáo để đưa ra quyết định viết thêm test có ý nghĩa.

Bài tập 4: Kiểm soát dữ liệu cũ

Chạy lại một test project sau khi sửa test. Kiểm tra xem thư mục kết quả có chứa nhiều file Coverage từ các lần chạy khác nhau hay không.

Thử chạy ReportGenerator với phạm vi đầu vào chỉ chứa kết quả của lần chạy mới, rồi so sánh với báo cáo được tạo từ cả kết quả cũ.

Câu hỏi: Vì sao việc chọn đúng file đầu vào lại quan trọng khi đọc và so sánh Coverage?

12. Tổng kết

Trong bài này, chúng ta đã mở rộng từ một test project thành solution có nhiều test project và học cách quản lý kết quả Coverage.

  • Coverlet thu thập dữ liệu Coverage trong lúc chạy test.
  • Mỗi lần chạy có thể tạo một file coverage.cobertura.xml trong thư mục kết quả riêng.
  • ReportGenerator tạo báo cáo HTML từ một hoặc nhiều file Coverage.
  • Báo cáo riêng giúp đánh giá từng nhóm test; báo cáo tổng hợp giúp quan sát Coverage từ nhiều nguồn kết quả đã chọn.
  • Cần tránh lấy nhầm file cũ hoặc tính tổng Coverage bằng cách cộng, lấy trung bình các phần trăm riêng.

Thông điệp quan trọng: Code Coverage không chỉ là một con số phần trăm. Giá trị của nó nằm ở khả năng giúp bạn xác định phần code chưa được kiểm tra và lựa chọn test case phù hợp để cải thiện chất lượng phần mềm.

Tài liệu tham khảo

Nhận xét

Bài đăng phổ biến từ blog này

[ASP.NET MVC] Authentication và Authorize

Một trong những vấn đề bảo mật cơ bản nhất là đảm bảo những người dùng hợp lệ truy cập vào hệ thống. ASP.NET đưa ra 2 khái niệm: Authentication và Authorize Authentication xác nhận bạn là ai. Ví dụ: Bạn có thể đăng nhập vào hệ thống bằng username và password hoặc bằng ssh. Authorization xác nhận những gì bạn có thể làm. Ví dụ: Bạn được phép truy cập vào website, đăng thông tin lên diễn đàn nhưng bạn không được phép truy cập vào trang mod và admin.

Tổng hợp một số kiến thức lập trình về Amibroker

Giới thiệu về Amibroker Amibroker theo developer Tomasz Janeczko được xây dựng dựa trên ngôn ngữ C. Vì vậy bộ code Amibroker Formula Language sử dụng có syntax khá tương đồng với C, ví dụ như câu lệnh #include để import hay cách gói các object, hàm trong các block {} và kết thúc câu lệnh bằng dấu “;”. AFL trong Amibroker là ngôn ngữ xử lý mảng (an array processing language). Nó hoạt động dựa trên các mảng (các dòng/vector) số liệu, khá giống với cách hoạt động của spreadsheet trên excel.

ASP.NET MVC: Cơ bản về Validation

Validation (chứng thực) là một tính năng quan trọng trong ASP.NET MVC và được phát triển trong một thời gian dài. Validation vắng mặt trong phiên bản đầu tiên của asp.net mvc và thật khó để tích hợp 1 framework validation của một bên thứ 3 vì không có khả năng mở rộng. ASP.NET MVC2 đã hỗ trợ framework validation do Microsoft phát triển, tên là Data Annotations. Và trong phiên bản 3, framework validation đã hỗ trợ tốt hơn việc xác thực phía máy khách, và đây là một xu hướng của việc phát triển ứng dụng web ngày nay.