Tạo các bài kiểm tra unit test được tham số hóa trong xUnit với Theory, InlineData, ClassData và MemberData

Trong bài viết này, tôi sẽ giới thiệu về cách tạo các bài kiểm tra unit test được tham số hóa bằng cách sử dụng attribute [Theory] của xUnit và cách bạn có thể truyền dữ liệu vào các phương thức kiểm tra của mình.

Tôi cũng sẽ đề cập đến thuộc tính [InlineData], [ClassData] và [MemberData]. Trong bài viết tiếp theo, tôi sẽ hướng dẫn cách tải dữ liệu theo những cách khác bằng cách tạo [DataAttribute] của riêng bạn.

Nếu bạn mới làm việc với xUnit, tôi khuyên bạn nên đọc bài viết này. Phần này cho thấy cách bắt đầu thử nghiệm các dự án .NET Core với xUnit, đồng thời cung cấp phần giới thiệu thử nghiệm sử dụng [Fact][Theory].

Viết unit test sử dụng thuộc tính Fact của xUnit

Nếu chúng ta viết một số bài kiểm tra unit test, thì dễ nhất là chúng ta có một cái gì đó chúng ta muốn kiểm tra. Tôi sẽ sử dụng chương trình máy tính cực kỳ đơn giản bên dưới để viết unit test:

public class Calculator
{
    public int Add(int value1, int value2)
    {
        return value1 + value2;
    }
}

Phương thức Add có hai tham số đầu vào, cộng chúng lại với nhau và trả về kết quả.

Chúng ta sẽ bắt đầu bằng cách tạo thử nghiệm xUnit đầu tiên của chúng ta cho lớp này. Trong xUnit, phương pháp kiểm tra cơ bản nhất là một phương thức không tham số công khai được gắn thuộc tính [Fact].

Ví dụ sau kiểm tra xem khi chúng ta truyền các giá trị 1 và 2 cho phương thức Add(), nó sẽ trả về 3:

public class CalculatorTests
{
    [Fact]
    public void CanAdd()
    {
        var calculator = new Calculator();

        int value1 = 1;
        int value2 = 2;

        var result = calculator.Add(value1, value2);

        Assert.Equal(3, result);
    }
}

Nếu bạn chạy dự án thử nghiệm của mình bằng lệnh dotnet test (hoặc Test Explorer của Visual Studio), thì bạn sẽ thấy một thử nghiệm duy nhất được liệt kê, cho thấy thử nghiệm đã được vượt qua:

Viết unit test sử dụng thuộc tính [Fact] của xUnit

Chúng ta biết rằng phương thức Calculator.Add() đang hoạt động chính xác với các giá trị cụ thể này, nhưng rõ ràng chúng ta sẽ cần phải kiểm tra nhiều giá trị hơn.

Câu hỏi đặt ra là cách tốt nhất để đạt được điều này là gì? Chúng ta có thể copy và paste bài kiểm tra này và chỉ cần thay đổi tên phương thức kiểm tra, thay đổi giá trị cho các tham số đầu vào, nhưng giải pháp này không ổn tý nào. Thay vào đó, xUnit cung cấp thuộc tính [Theory] cho tình huống này.

Sử dụng thuộc tính Theory và InlineData để tạo các thử nghiệm được tham số hóa

xUnit sử dụng thuộc tính [Fact] để biểu thị kiểm tra unit test không tham số, kiểm tra các bất biến trong mã của bạn.

Ngược lại, thuộc tính [Theory] biểu thị một thử nghiệm được tham số hóa với một tập con dữ liệu. Dữ liệu đó có thể được cung cấp theo một số cách, nhưng phổ biến nhất là với thuộc tính [InlineData].

Ví dụ sau cho thấy cách bạn có thể viết lại phương thức kiểm tra CanAdd trước đó để sử dụng thuộc tính [Theory], [InlineData] và thêm một số giá trị bổ sung để kiểm tra:

[Theory]
[InlineData(1, 2, 3)]
[InlineData(-4, -6, -10)]
[InlineData(-2, 2, 0)]
[InlineData(int.MinValue, -1, int.MaxValue)]
public void CanAddTheory(int value1, int value2, int expected)
{
    var calculator = new Calculator();

    var result = calculator.Add(value1, value2);

    Assert.Equal(expected, result);
}

Thay vì chỉ định các giá trị để tính tổng (value1value2) trong phần thân của bài kiểm tra unit test, chúng ta truyền các giá trị đó làm tham số cho thử nghiệm. Chúng ta cũng truyền kết quả dự kiến ​​của phép tính để sử dụng trong phương thức Assert.Equal().

Dữ liệu được cung cấp bởi thuộc tính [InlineData]. Mỗi một khai báo [InlineData] sẽ tạo ra một phương thức CanAddTheory được thực thi riêng biệt.

Các giá trị được truyền trong hàm khởi tạo của [InlineData] được sử dụng làm tham số cho phương thức - thứ tự của các tham số trong thuộc tính khớp với thứ tự mà chúng được cung cấp cho phương thức.

Tips: Gói xUnit 2.3.0 NuGet bao gồm một số bộ phân tích Roslyn có thể giúp đảm bảo rằng các tham số của [InlineData] khớp với các tham số của phương thức. Hình ảnh dưới đây cho thấy ba lỗi: không đủ tham số, quá nhiều tham số và tham số không đúng kiểu dữ liệu.

Nếu bạn chạy các bài kiểm tra cho phương thức này, bạn sẽ thấy mỗi khai báo [InlineData] tạo ra một phiên bản riêng biệt. xUnit thêm thủ công các tên và giá trị tham số vào mô tả thử nghiệm, vì vậy bạn có thể dễ dàng xem lần lặp nào không thành công.

Ngoài ra, bạn có thấy tôi đã làm gì với bài kiểm tra với int.MinValue không? Bạn đang kiểm tra các trường hợp biên của bạn hoạt động như mong đợi phải không?

Các thuộc tính [InlineData] hoàn hảo khi các tham số phương thức của bạn là hằng số, và bạn không có quá nhiều trường hợp để kiểm tra. Nếu không phải như vậy, thì bạn có thể muốn xem xét một trong những cách khác để cung cấp dữ liệu cho các phương thức [Theory] của mình.

Sử dụng một lớp dữ liệu chuyên dụng với ClassData

Nếu các giá trị bạn cần truyền cho bài kiểm tra [Theory] không phải là hằng số, thì bạn có thể sử dụng một thuộc tính thay thế là [ClassData] để cung cấp các tham số. Thuộc tính này nhận tham số đầu vào kiểu Type để xUnit sử dụng để lấy dữ liệu:

[Theory]
[ClassData(typeof(CalculatorTestData))]
public void CanAddTheoryClassData(int value1, int value2, int expected)
{
    var calculator = new Calculator();

    var result = calculator.Add(value1, value2);

    Assert.Equal(expected, result);
}

Chúng tôi đã chỉ định kiểu dữ liệu CalculatorTestData trong thuộc tính [ClassData]. Lớp này phải triển khai interface IEnumerable<object[]>, trong đó mỗi phần tử được trả về là một mảng object để sử dụng làm tham số phương thức.

Chúng ta có thể viết lại dữ liệu từ thuộc tính [InlineData] bằng cách sử dụng phương pháp này:

public class CalculatorTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 1, 2, 3 };
        yield return new object[] { -4, -6, -10 };
        yield return new object[] { -2, 2, 0 };
        yield return new object[] { int.MinValue, -1, int.MaxValue };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Rõ ràng là bạn có thể viết danh sách này theo nhiều cách, nhưng tôi đã chọn một cách tiếp cận đơn giản. xUnit sẽ gọi phương thức .ToList() trên lớp được bạn cung cấp trước khi nó chạy bất kỳ trường hợp phương thức Theory nào, vì vậy điều quan trọng là dữ liệu phải độc lập. Bạn không muốn có các đối tượng được chia sẻ giữa các lần chạy thử nghiệm gây ra lỗi kỳ lạ!

Các thuộc tính [ClassData] là một cách thuận tiện để loại bỏ sự lộn xộn từ các tập tin thử nghiệm của bạn, nhưng nếu bạn không muốn tạo ra thêm một lớp? Đối với những tình huống này, bạn có thể sử dụng thuộc tính [MemberData].

Sử dụng thuộc tính của trình tạo với các thuộc tính MemberData

Các thuộc tính [MemberData] có thể được sử dụng để lấy dữ liệu cho một thử nghiệm [Theory] từ một thuộc tính hoặc phương thức static của một kiểu dữ liệu. Thuộc tính này có khá nhiều tùy chọn, vì vậy tôi sẽ chỉ lướt qua một số tùy chọn trong số đó ở đây.

Tải dữ liệu từ một thuộc tính của lớp thử nghiệm

Các thuộc tính [MemberData] có thể tải dữ liệu từ một thuộc tính IEnnumerable<object[]> của lớp thử nghiệm. Bộ phân tích xUnit sẽ xác định bất kỳ vấn đề nào với cấu hình của bạn, chẳng hạn như thuộc tính bị thiếu hoặc sử dụng thuộc tính trả về kiểu dữ liệu không hợp lệ.

Trong ví dụ sau, tôi đã thêm một thuộc tính Data trả về kiểu IEnumerable<object[]>, tương tự như ví dụ [ClassData]:

public class CalculatorTests
{
    [Theory]
    [MemberData(nameof(Data))]
    public void CanAddTheoryMemberDataProperty(int value1, int value2, int expected)
    {
        var calculator = new Calculator();

        var result = calculator.Add(value1, value2);

        Assert.Equal(expected, result);
    }

    public static IEnumerable<object[]> Data =>
        new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };
}

Tải dữ liệu từ một phương thức của lớp thử nghiệm

Cũng như các thuộc tính, bạn có thể truyền dữ liệu cho [MemberData] từ một phương thức static. Các phương thức này thậm chí có thể có tham số. Trong trường hợp này, bạn cần cung cấp các tham số trong [MemberData], như dưới đây:

public class CalculatorTests
{
    [Theory]
    [MemberData(nameof(GetData), parameters: 3)]
    public void CanAddTheoryMemberDataMethod(int value1, int value2, int expected)
    {
        var calculator = new Calculator();

        var result = calculator.Add(value1, value2);

        Assert.Equal(expected, result);
    }

    public static IEnumerable<object[]> GetData(int numTests)
    {
        var allData = new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };

        return allData.Take(numTests);
    }
}

Trong trường hợp này, xUnit gọi GetData() đầu tiên, truyền vào tham số là numTests: 3. Sau đó, nó sử dụng từng được object[] trả về bởi phương thức để thực hiện kiểm tra [Theory].

Tải dữ liệu từ một thuộc tính hoặc phương thức từ một lớp khác

Tùy chọn này là một loại kết hợp giữa thuộc tính [ClassData][MemberData]. Thay vì tải dữ liệu từ thuộc tính hoặc phương thức của lớp thử nghiệm, bạn tải dữ liệu từ thuộc tính hoặc phương thức từ một số kiểu được chỉ định khác:

public class CalculatorTests
{
    [Theory]
    [MemberData(nameof(CalculatorData.Data), MemberType= typeof(CalculatorData))]
    public void CanAddTheoryMemberDataMethod(int value1, int value2, int expected)
    {
        var calculator = new Calculator();

        var result = calculator.Add(value1, value2);

        Assert.Equal(expected, result);
    }
}

public class CalculatorData
{
    public static IEnumerable<object[]> Data =>
        new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };
}

Có khá nhiều tùy chọn để cung cấp dữ liệu cho các bài kiểm tra [Theory]. Nếu các thuộc tính này không cho phép bạn cung cấp dữ liệu theo cách bạn muốn, bạn luôn có thể tạo dữ liệu của riêng mình, bạn sẽ thấy trong bài viết tiếp theo của tôi.

Unit TestXUnit
Bài Viết Liên Quan:
E2E Test với ASP.NET Core, XUnit và Playwright
Trung Nguyen 15/10/2021
E2E Test với ASP.NET Core, XUnit và Playwright

Trong bài viết này, chúng ta sẽ tìm hiểu cách sử dụng thư viện Playwright kết hợp với XUnit để kiểm tra các ứng dụng web ASP.NET Core như người dùng có thể.

Tạo DataAttribute tùy chỉnh cho Theory của xUnit để tải dữ liệu từ file JSON
Trung Nguyen 22/04/2021
Tạo DataAttribute tùy chỉnh cho Theory của xUnit để tải dữ liệu từ file JSON

Trong bài viết này hướng dẫn bạn cách tạo lớp DataAttribute tùy chỉnh để tải dữ liệu cho bài kiểm tra unit test viết bằng xUnit của bạn.

Sử dụng Theory và InlineData của xUnit để viết unit test
Trung Nguyen 18/04/2021
Sử dụng Theory và InlineData của xUnit để viết unit test

Trong bài này, chúng ta sẽ sử dụng thuộc tính [Theory] và [InlineData] của xUnit để nhanh chóng viết một loạt các bài kiểm tra unit test.

Unit test cho controller trong ASP.NET Core với xUnit và Moq
Trung Nguyen 18/04/2021
Unit test cho controller trong ASP.NET Core với xUnit và Moq

Trong bài viết này, chúng ta sẽ viết một số bài kiểm tra unit test cho Controller của ứng dụng ASP.NET sử dụng xUnit và Moq.