Chạy tất cả Unit Test và nhận kết quả Passed có đồng nghĩa với việc toàn bộ code đã được kiểm tra?
Câu trả lời là không.
Hãy tưởng tượng bạn vừa hoàn thành một chức năng mới trong ứng dụng .NET. Bạn chạy toàn bộ Unit Test và nhận được kết quả:
Passed: 120
Failed: 0
Mọi test đều thành công. Tuy nhiên, có thể một method mới chưa được bất kỳ test nào gọi đến, hoặc một nhánh xử lý quan trọng chưa bao giờ được thực thi.
Nếu không có thêm thông tin, chúng ta khó biết được những phần code nào thực sự đã được kiểm tra.
Trong bài viết này, chúng ta sẽ tìm hiểu Code Coverage, tạo Coverage Report cho một project .NET và sử dụng báo cáo để tìm những phần code chưa được test.
1. Code Coverage là gì?
Code Coverage là một phép đo cho biết phần code có thể thực thi nào đã được chạy trong quá trình thực hiện test.
Ví dụ, một ứng dụng có 100 dòng code có thể thực thi. Trong quá trình chạy test, 70 dòng được thực thi. Line Coverage có thể đạt 70%.
Con số này giúp chúng ta nhận biết những khu vực chưa được test, nhưng không có nghĩa 70 dòng đó đã được kiểm tra đúng về mặt nghiệp vụ.
Hãy phân biệt hai khái niệm:
- Test Result: Các test có thành công hay thất bại?
- Code Coverage: Những phần code nào đã được thực thi khi chạy test?
Hai thông tin này bổ sung cho nhau. Test Passed cho biết các kiểm tra trong test đã thành công; Coverage giúp phát hiện những phần code mà test suite chưa chạm tới.
2. Tạo project demo
Chúng ta sẽ xây dựng một ví dụ nhỏ về hệ thống quản lý thư viện. Khi thành viên trả sách muộn, thư viện tính phí 5.000 VNĐ cho mỗi ngày trễ.
Các quy tắc nghiệp vụ gồm:
- Trả sách đúng hạn: không mất phí.
- Trả sách muộn: phí bằng số ngày trễ nhân với 5.000 VNĐ.
- Số ngày trễ âm là dữ liệu không hợp lệ và phải phát sinh exception.
Đầu tiên, tạo solution và hai project bằng .NET CLI. Các lệnh sau có thể chạy trong PowerShell:
dotnet new sln -n LibraryManagement
dotnet new classlib -n LibraryManagement
dotnet new xunit -n LibraryManagement.Tests
dotnet sln LibraryManagement.slnx add LibraryManagement/LibraryManagement.csproj
dotnet sln LibraryManagement.slnx add LibraryManagement.Tests/LibraryManagement.Tests.csproj
dotnet add LibraryManagement.Tests/LibraryManagement.Tests.csproj reference LibraryManagement/LibraryManagement.csproj
Nếu SDK của bạn tạo solution với tên hoặc cấu trúc khác, hãy sử dụng đúng tên file solution được tạo trên máy.
Cấu trúc project lúc này:
LibraryManagement/
├── LibraryManagement.slnx
├── LibraryManagement/
│ ├── LibraryManagement.csproj
│ └── Class1.cs
└── LibraryManagement.Tests/
├── LibraryManagement.Tests.csproj
└── UnitTest1.cs
Xóa hai file mẫu Class1.cs và UnitTest1.cs. Sau đó tạo các file dưới đây.
3. Viết code nghiệp vụ
Tạo file LibraryService.cs trong project LibraryManagement:
namespace LibraryManagement;
public class LibraryService
{
private const decimal DailyFine = 5000m;
public decimal CalculateLateFee(int overdueDays)
{
if (overdueDays < 0)
{
throw new ArgumentException(
"Overdue days cannot be negative.");
}
if (overdueDays == 0)
{
return 0;
}
return overdueDays * DailyFine;
}
}
Hãy đọc method CalculateLateFee() theo thứ tự thực thi:
- Nếu số ngày trễ nhỏ hơn 0, method phát sinh
ArgumentException. - Nếu số ngày trễ bằng 0, method trả về 0.
- Trong các trường hợp còn lại, method tính phí bằng số ngày trễ nhân với 5.000.
Method này có nhiều đường xử lý. Nếu test chỉ kiểm tra trường hợp trả sách đúng hạn, những phần code còn lại có thể chưa được thực thi.
4. Viết Unit Test đầu tiên
Trong project LibraryManagement.Tests, tạo file LibraryServiceTests.cs:
using LibraryManagement;
using Xunit;
namespace LibraryManagement.Tests;
public class LibraryServiceTests
{
[Fact]
public void CalculateLateFee_WhenReturnedOnTime_ReturnsZero()
{
var service = new LibraryService();
var result = service.CalculateLateFee(0);
Assert.Equal(0m, result);
}
}
Test này kiểm tra một trường hợp: thành viên trả sách đúng hạn thì phí trễ hạn phải bằng 0.
Hãy chú ý ba bước trong test:
- Arrange: Tạo đối tượng
LibraryService. - Act: Gọi
CalculateLateFee(0). - Assert: Kiểm tra kết quả bằng
0m.
Đây là cấu trúc Arrange – Act – Assert thường được sử dụng để viết Unit Test dễ đọc.
Chạy test:
dotnet test
Nếu project được tạo và tham chiếu đúng, test sẽ Passed. Tuy nhiên, chúng ta mới chỉ xác nhận một trường hợp nghiệp vụ. Bước tiếp theo là kiểm tra phạm vi code được thực thi.
Kết quả:> dotnet test
Restore complete (0.4s)
LibraryManagement net10.0 succeeded (0.2s) → D:\Practices\Test\LibraryManagement\LibraryManagement\bin\Debug\net10.0\LibraryManagement.dll
LibraryManagement.Tests net10.0 succeeded (0.2s) → bin\Debug\net10.0\LibraryManagement.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.4+50e68bbb8b (64-bit .NET 10.0.13)
[xUnit.net 00:00:00.40] Discovering: LibraryManagement.Tests
[xUnit.net 00:00:00.45] Discovered: LibraryManagement.Tests
[xUnit.net 00:00:00.47] Starting: LibraryManagement.Tests
[xUnit.net 00:00:00.63] Finished: LibraryManagement.Tests
LibraryManagement.Tests test net10.0 succeeded (3.2s)
Test summary: total: 1, failed: 0, succeeded: 1, skipped: 0, duration: 3.2s
Build succeeded in 4.6s
5. Thu thập Code Coverage bằng .NET CLI
Để thu thập Coverage, trước tiên cần cài package coverlet.collector vào project test:
dotnet add LibraryManagement.Tests/LibraryManagement.Tests.csproj package coverlet.collector
Package này cung cấp collector có tên XPlat Code Coverage, cho phép thu thập dữ liệu Coverage khi chạy test.
Bây giờ, chạy lệnh:
dotnet test LibraryManagement.Tests/LibraryManagement.Tests.csproj --collect:"XPlat Code Coverage"
Nếu test thành công, kết quả sẽ bao gồm thông tin về test và thư mục chứa dữ liệu Coverage. Đường dẫn có thể tương tự như sau:
LibraryManagement.Tests/
└── TestResults/
└── <test-run-id>/
└── coverage.cobertura.xml
Phần <test-run-id> đại diện cho thư mục được tạo cho lần chạy test. Tên thư mục thực tế sẽ khác nhau giữa các lần chạy.
File coverage.cobertura.xml chứa dữ liệu Coverage ở định dạng Cobertura. Đây là dữ liệu đầu vào cho công cụ tạo báo cáo; nó không phải báo cáo HTML dễ đọc dành cho người dùng.
6. Tạo báo cáo HTML bằng ReportGenerator
Để xem Coverage thuận tiện hơn, chúng ta sẽ dùng ReportGenerator để chuyển file XML thành báo cáo HTML.
Cài công cụ bằng lệnh:
dotnet tool install --global dotnet-reportgenerator-globaltool
Nếu công cụ đã được cài đặt trước đó, bạn không cần cài lại. Có thể kiểm tra bằng:
reportgenerator -version
Tiếp theo, chạy test và tạo report từ thư mục gốc của solution:
dotnet test LibraryManagement.Tests/LibraryManagement.Tests.csproj --collect:"XPlat Code Coverage"
reportgenerator "-reports:LibraryManagement.Tests/TestResults/**/coverage.cobertura.xml" "-targetdir:CoverageReport" "-reporttypes:Html"
Trong lệnh trên:
-reports: chỉ định file Coverage XML đầu vào. Ký tự**giúp tìm file trong các thư mục con.-targetdir: chỉ định thư mục chứa báo cáo được tạo ra.-reporttypes:Html: yêu cầu tạo báo cáo HTML.
Nếu đường dẫn project hoặc thư mục kết quả trên máy bạn khác ví dụ, hãy điều chỉnh đường dẫn tương ứng.
Sau khi lệnh hoàn tất, mở file:
CoverageReport/index.html
Báo cáo sẽ hiển thị các thông tin như tỷ lệ Coverage, danh sách class và chi tiết code được thực thi. Tùy phiên bản và cấu hình công cụ, giao diện có thể khác nhau.
7. Đọc báo cáo và tìm code chưa được test
Quay lại LibraryService.CalculateLateFee(). Trong test hiện tại, chúng ta chỉ truyền overdueDays = 0.
Điều gì xảy ra khi test chạy?
flowchart TD
A["CalculateLateFee(0)"] --> B{"overdueDays < 0?"}
B -->|False| C{"overdueDays == 0?"}
C -->|True| D["return 0"]
D --> E["Test Passed"]
B -->|True| F["Throw ArgumentException"]
C -->|False| G["Calculate late fee"]
Luồng thực thi cho thấy test hiện tại đi qua nhánh trả về 0. Hai đường xử lý còn lại chưa được kiểm tra bằng test này.
Trong Coverage Report, hãy tìm class LibraryService và xem các dòng được đánh dấu là covered hoặc uncovered. ReportGenerator thường sử dụng màu sắc để phân biệt các dòng đã và chưa được thực thi.
Với test hiện tại, chúng ta kỳ vọng:
- Điều kiện kiểm tra số ngày âm đã được đánh giá, nhưng nhánh phát sinh exception chưa được chạy.
- Nhánh
overdueDays == 0được thực hiện. - Dòng tính phí trễ hạn chưa được thực hiện.
Lưu ý: Tỷ lệ Line Coverage và Branch Coverage cụ thể phải lấy từ báo cáo thực tế. Không nên mặc định rằng một test sẽ tạo ra một tỷ lệ phần trăm cố định, vì kết quả phụ thuộc vào các dòng được công cụ tính là executable và cấu hình đo lường.
8. Bổ sung test cho phần code còn thiếu
Sau khi xác định các khoảng trống, chúng ta có thể bổ sung test cho những trường hợp nghiệp vụ quan trọng.
Thêm hai test sau vào LibraryServiceTests.cs:
[Fact]
public void CalculateLateFee_WhenOverdue_ReturnsExpectedFee()
{
var service = new LibraryService();
var result = service.CalculateLateFee(3);
Assert.Equal(15000m, result);
}
[Fact]
public void CalculateLateFee_WhenDaysAreNegative_ThrowsException()
{
var service = new LibraryService();
Assert.Throws<ArgumentException>(
() => service.CalculateLateFee(-1));
}
Hai test này kiểm tra hai hành vi khác nhau:
- Trả sách muộn 3 ngày phải chịu phí 15.000 VNĐ.
- Số ngày trễ âm phải phát sinh
ArgumentException.
Chạy lại test và tạo báo cáo:
dotnet test LibraryManagement.Tests/LibraryManagement.Tests.csproj --collect:"XPlat Code Coverage"
reportgenerator "-reports:LibraryManagement.Tests/TestResults/**/coverage.cobertura.xml" "-targetdir:CoverageReport" "-reporttypes:Html"
Mở lại CoverageReport/index.html để so sánh kết quả với lần trước. Sau khi bổ sung test, các nhánh xử lý quan trọng của method sẽ được thực thi nhiều hơn.
Nếu muốn so sánh chính xác hai lần chạy, hãy lưu báo cáo của lần đầu vào một thư mục riêng trước khi tạo báo cáo mới.
9. Coverage cao có đồng nghĩa với code không có bug?
Không. Code Coverage chỉ cho biết phần code nào đã được thực thi, chứ không chứng minh rằng test đã kiểm tra đầy đủ mọi hành vi.
Ví dụ, nếu một test gọi method tính phí nhưng không kiểm tra kết quả bằng assertion phù hợp, method vẫn có thể được tính là covered. Tuy nhiên, test đó có thể không phát hiện được lỗi tính toán.
Vì vậy, khi đọc Coverage Report, hãy đặt ba câu hỏi:
- Những dòng code nào chưa được thực thi?
- Những nhánh nghiệp vụ nào chưa được kiểm tra?
- Các test hiện tại có thực sự xác nhận kết quả mong đợi không?
Không nhất thiết phải đạt 100% Coverage cho mọi project. Điều quan trọng là ưu tiên kiểm tra các business rule quan trọng, những trường hợp biên và các đường xử lý lỗi có ảnh hưởng lớn.
10. Tổng kết
Trong bài viết này, chúng ta đã:
- Phân biệt Test Result và Code Coverage.
- Tạo một project .NET nhỏ để tính phí trả sách trễ hạn.
- Thu thập Coverage bằng
dotnet test --collect:"XPlat Code Coverage". - Sử dụng ReportGenerator để tạo báo cáo HTML.
- Đọc báo cáo để tìm code chưa được test và bổ sung test tương ứng.
Thông điệp quan trọng nhất là: Test Passed không có nghĩa là toàn bộ code đã được kiểm tra. Coverage giúp chúng ta nhận biết những khoảng trống, còn các test với assertion phù hợp mới giúp xác nhận hành vi của chương trình.
Ở bài tiếp theo, chúng ta sẽ tìm hiểu sâu hơn về Line Coverage và Branch Coverage, đồng thời khám phá vì sao một method có Line Coverage cao vẫn có thể bỏ sót những nhánh logic quan trọng.
Nhận xét