Giới Thiệu
Nếu bạn là một lập trình viên C# đang tìm cách cải thiện chất lượng mã của mình, kiểm thử đơn vị sẽ là người bạn đồng hành tốt nhất. Trong hướng dẫn này, chúng ta sẽ cùng khám phá tất cả các loại kiểm thử đơn vị mà bạn cần biết, kèm theo ví dụ thực tiễn có thể áp dụng ngay lập tức.
Kiểm Thử Đơn Vị Là Gì?
Kiểm thử đơn vị là các chương trình nhỏ nhằm xác minh rằng một phần cụ thể của mã (một hàm, một phương thức) hoạt động đúng. Bạn có thể nghĩ về chúng như một hệ thống kiểm soát chất lượng tự động chạy mỗi khi bạn thay đổi một thứ gì đó trong mã.
Tại sao chúng lại quan trọng?
- Phát hiện lỗi sớm
- Tăng cường tự tin khi tái cấu trúc mã
- Tài liệu hóa cách mà mã của bạn nên hoạt động
- Giảm thời gian gỡ lỗi
1. Kiểm Thử Đơn Vị Cơ Bản
Hãy bắt đầu với các nguyên tắc cơ bản. Một kiểm thử đơn vị cơ bản theo mẫu AAA (Arrange-Act-Assert):
csharp
using NUnit.Framework;
[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
// Arrange: Chuẩn bị dữ liệu
var calculator = new Calculator();
int a = 5, b = 3;
// Act: Thực hiện hành động
int result = calculator.Add(a, b);
// Assert: Xác minh kết quả
Assert.AreEqual(8, result);
}
}
Các framework phổ biến trong C#:
- xUnit: Hiện đại và được khuyến nghị nhất
- NUnit: Rất toàn diện và linh hoạt
- MSTest: Tích hợp với Visual Studio
2. Kiểm Thử Với Mocks và Stubs
Khi mã của bạn phụ thuộc vào các dịch vụ bên ngoài (cơ sở dữ liệu, API, tệp), bạn cần "mô phỏng" những phụ thuộc đó để tách biệt những gì bạn thực sự muốn kiểm thử.
csharp
using Moq;
using NUnit.Framework;
public class OrderServiceTests
{
[Test]
public void ProcessOrder_ValidOrder_CallsPaymentService()
{
// Arrange
var mockPaymentService = new Mock<IPaymentService>();
var mockEmailService = new Mock<IEmailService>();
var orderService = new OrderService(mockPaymentService.Object, mockEmailService.Object);
var order = new Order { Amount = 100, CustomerEmail = "test@email.com" };
// Act
orderService.ProcessOrder(order);
// Assert
mockPaymentService.Verify(x => x.ProcessPayment(100), Times.Once);
mockEmailService.Verify(x => x.SendConfirmation("test@email.com"), Times.Once);
}
}
Khi nào nên sử dụng Mocks:
- Đối với các phụ thuộc bên ngoài (API, cơ sở dữ liệu)
- Đối với các dịch vụ tốn kém để tạo
- Để kiểm soát hành vi trong những kịch bản cụ thể
3. Kiểm Thử Tham Số
Tại sao phải viết 10 kiểm thử tương tự khi bạn có thể viết 1? Kiểm thử tham số cho phép bạn thực hiện cùng một logic với dữ liệu khác nhau:
csharp
[TestCase(0, 0, 0)]
[TestCase(1, 1, 2)]
[TestCase(-1, 1, 0)]
[TestCase(100, -50, 50)]
public void Add_DifferentInputs_ReturnsExpectedResult(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.AreEqual(expected, result);
}
// Bạn cũng có thể sử dụng TestCaseSource cho các trường hợp phức tạp hơn
[TestCaseSource(nameof(DivisionTestCases))]
public void Divide_VariousInputs_ReturnsCorrectResult(decimal dividend, decimal divisor, decimal expected)
{
var calculator = new Calculator();
var result = calculator.Divide(dividend, divisor);
Assert.AreEqual(expected, result, 0.001m);
}
private static IEnumerable<TestCaseData> DivisionTestCases()
{
yield return new TestCaseData(10m, 2m, 5m);
yield return new TestCaseData(7m, 3m, 2.333m);
yield return new TestCaseData(-6m, 2m, -3m);
}
4. Kiểm Thử Ngoại Lệ
Mã của bạn cần xử lý lỗi một cách hợp lý. Những kiểm thử này xác minh rằng các ngoại lệ được ném ra khi chúng nên có:
csharp
[Test]
public void Divide_ByZero_ThrowsArgumentException()
{
var calculator = new Calculator();
var exception = Assert.Throws<ArgumentException>(
() => calculator.Divide(10, 0)
);
Assert.That(exception.Message, Contains.Substring("divisor cannot be zero"));
}
[Test]
public void CreateUser_NullEmail_ThrowsArgumentNullException()
{
var userService = new UserService();
Assert.Throws<ArgumentNullException>(
() => userService.CreateUser(null, "password")
);
}
5. Kiểm Thử Asynchronous
Với async/await hiện đang rất phổ biến, bạn cần biết cách kiểm thử mã bất đồng bộ:
csharp
[Test]
public async Task GetUserAsync_ValidId_ReturnsUser()
{
// Arrange
var mockRepository = new Mock<IUserRepository>();
mockRepository.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(new User { Id = 1, Name = "John" });
var userService = new UserService(mockRepository.Object);
// Act
var user = await userService.GetUserAsync(1);
// Assert
Assert.IsNotNull(user);
Assert.AreEqual(1, user.Id);
Assert.AreEqual("John", user.Name);
}
[Test]
public async Task SaveDataAsync_DatabaseTimeout_ThrowsTimeoutException()
{
var mockRepository = new Mock<IDataRepository>();
mockRepository.Setup(x => x.SaveAsync(It.IsAny<Data>()))
.ThrowsAsync(new TimeoutException());
var service = new DataService(mockRepository.Object);
await Assert.ThrowsAsync<TimeoutException>(
() => service.SaveDataAsync(new Data())
);
}
6. Kiểm Thử Trạng Thái và Hành Vi
Kiểm Thử Trạng Thái: Xác minh trạng thái cuối cùng của đối tượng
csharp
[Test]
public void AddItem_ToCart_IncreasesItemCount()
{
var cart = new ShoppingCart();
cart.AddItem(new Item("Laptop", 1000));
Assert.AreEqual(1, cart.ItemCount);
Assert.AreEqual(1000, cart.TotalAmount);
}
Kiểm Thử Hành Vi: Xác minh rằng các phương thức đúng đã được gọi
csharp
[Test]
public void ProcessOrder_ValidOrder_CallsRequiredServices()
{
var mockInventory = new Mock<IInventoryService>();
var mockPayment = new Mock<IPaymentService>();
var processor = new OrderProcessor(mockInventory.Object, mockPayment.Object);
processor.ProcessOrder(new Order());
mockInventory.Verify(x => x.ReserveItems(It.IsAny<Order>()), Times.Once);
mockPayment.Verify(x => x.ProcessPayment(It.IsAny<Order>()), Times.Once);
}
7. Kiểm Thử Dựa Trên Thuộc Tính
Một kỹ thuật nâng cao nơi bạn tự động tạo ra các trường hợp kiểm thử:
csharp
using FsCheck;
using FsCheck.NUnit;
[Property]
public bool Add_IsCommutative(int a, int b)
{
var calculator = new Calculator();
return calculator.Add(a, b) == calculator.Add(b, a);
}
[Property]
public bool Sort_ListIsOrdered(int[] input)
{
var sorted = input.OrderBy(x => x).ToArray();
for (int i = 0; i < sorted.Length - 1; i++)
{
if (sorted[i] > sorted[i + 1])
return false;
}
return true;
}
8. Kiểm Thử Tích Hợp Nhẹ
Mặc dù không hoàn toàn là kiểm thử đơn vị, nhưng đôi khi bạn cần kiểm thử cách nhiều thành phần tương tác:
csharp
[Test]
public void UserRegistration_EndToEndFlow_WorksCorrectly()
{
// Sử dụng cơ sở dữ liệu trong bộ nhớ
var options = new DbContextOptionsBuilder<UserContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
using var context = new UserContext(options);
var repository = new UserRepository(context);
var emailService = new Mock<IEmailService>();
var userService = new UserService(repository, emailService.Object);
// Act
var result = userService.RegisterUser("john@email.com", "password123");
// Assert
Assert.IsTrue(result.Success);
Assert.AreEqual(1, context.Users.Count());
emailService.Verify(x => x.SendWelcomeEmail("john@email.com"), Times.Once);
}
Thực Hành Tốt Nhất Cho Các Kiểm Thử Thành Công
1. Tên Mô Tả
csharp
// ❌ Kém
[Test]
public void Test1() { }
// ✅ Tốt
[Test]
public void CalculateDiscount_CustomerIsVIP_Returns20PercentDiscount() { }
2. Trách Nhiệm Đơn Nhất Cho Mỗi Kiểm Thử
csharp
// ❌ Kém - kiểm thử nhiều thứ
[Test]
public void UserService_Tests()
{
// Kiểm thử tạo, cập nhật, xóa...
}
// ✅ Tốt - mỗi kiểm thử có mục tiêu cụ thể
[Test]
public void CreateUser_ValidInput_ReturnsSuccessResult() { }
[Test]
public void CreateUser_DuplicateEmail_ReturnsErrorResult() { }
3. Dữ Liệu Kiểm Thử Rõ Ràng
csharp
// ❌ Kém - số ma thuật
[Test]
public void CalculateTotal_ReturnsCorrectAmount()
{
var result = calculator.Calculate(100, 0.15, 5);
Assert.AreEqual(120, result);
}
// ✅ Tốt - giá trị có ý nghĩa
[Test]
public void CalculateTotal_WithTaxAndShipping_ReturnsCorrectAmount()
{
const decimal baseAmount = 100m;
const decimal taxRate = 0.15m;
const decimal shipping = 5m;
const decimal expected = 120m;
var result = calculator.Calculate(baseAmount, taxRate, shipping);
Assert.AreEqual(expected, result);
}
4. Thiết Lập và Dọn Dẹp
csharp
[TestFixture]
public class DatabaseTests
{
private TestDatabase _database;
[SetUp]
public void Setup()
{
_database = new TestDatabase();
_database.Initialise();
}
[TearDown]
public void Cleanup()
{
_database.Cleanup();
_database.Dispose();
}
}
Công Cụ và Tiện Ích Hữu Ích
Framework Kiểm Thử
- xUnit.net: Hiện đại và có thể mở rộng
- NUnit: Trưởng thành với nhiều tính năng
- MSTest: Tích hợp gốc với Visual Studio
Giả Lập
- Moq: Phổ biến nhất
- NSubstitute: Cú pháp sạch hơn
- FakeItEasy: Dễ sử dụng
Đo Lường Độ Bao Phủ
- Coverlet: Cho .NET Core
- dotCover: Bởi JetBrains
- Visual Studio: Công cụ tích hợp
Trình Chạy Kiểm Thử
- Visual Studio Test Explorer
- ReSharper Unit Test Runner
- Rider: IDE hoàn chỉnh của JetBrains
Kết Luận
Kiểm thử đơn vị không chỉ là một "thực hành tốt" mà là điều cần thiết cho việc phát triển phần mềm đáng tin cậy và dễ bảo trì. Bắt đầu với các kiểm thử cơ bản và từ từ tích hợp các kỹ thuật nâng cao hơn.
Nhớ rằng:
- Kiểm thử cần nhanh và đáng tin cậy
- Tên mô tả có giá trị hơn hàng ngàn bình luận
- Mỗi kiểm thử nên có một mục đích duy nhất
- Mocks là bạn đồng hành của bạn trong việc tách biệt các phụ thuộc
- 100% độ bao phủ không đảm bảo chất lượng, nhưng các kiểm thử được viết tốt thì có
Bước tiếp theo của bạn? Hãy lấy một dự án hiện có và thêm kiểm thử cho một lớp nhỏ. Bạn sẽ ngay lập tức thấy được lợi ích và muốn tiếp tục viết thêm.