0
0
Lập trình
Admin Team
Admin Teamtechmely

Các Loại Kiểm Thử Đơn Vị trong C# - Nâng Cao Chất Lượng Mã

Đăng vào 1 tháng trước

• 8 phút đọc

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 Copy
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 Copy
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 Copy
[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 Copy
[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 Copy
[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 Copy
[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 Copy
[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 Copy
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 Copy
[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 Copy
// ❌ 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 Copy
// ❌ 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 Copy
// ❌ 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 Copy
[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.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào