Sử dụng Theory và InlineData của xUnit để viết unit test

Trong bài viết trước của loạt bài này, chúng ta đã tìm hiểu cách viết một số bài kiểm tra unit test cho Controller của ứng dụng ASP.NET Core MVC sử dụng xUnit và Moq.

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.

Trong phần cuối cùng của loạt bài kiểm tra unit test này, chúng ta sẽ sử dụng một phương thức mở rộng duy nhất và cách chúng ta có thể sử dụng thuộc tính [Theory][InlineData] của xUnit để nhanh chóng viết một loạt các bài kiểm tra cho phương thức đó.

Phương thức mở rộng

Chúng ta hãy xem xét phương thức mở rộng (và cách liệt kê liên quan của nó):

public enum CIEqualsOption : short
{
    Normal,
    NullEqualsEmpty
}

/// <summary>
/// Case-Insensitive Equals. Returns true if 
/// the two strings are equal (regardless of case).
/// </summary>
public static bool CIEquals(this string first, 
                            string second, 
                            CIEqualsOption option = CIEqualsOption.Normal)
{
    if (option == CIEqualsOption.NullEqualsEmpty)
    {
        return String.Equals(first ?? "", 
                             second ?? "", 
                             StringComparison.InvariantCultureIgnoreCase);
    }
    
    return String.Equals(first, 
                         second, 
                         StringComparison.InvariantCultureIgnoreCase);
}

Phương thức này là một phương thức so sánh chuỗi không phân biệt chữ hoa chữ thường; nó sẽ trả về true nếu hai chuỗi giống nhau, bỏ qua cách viết hoa. Ngoài ra còn có một tùy chọn để xử lý các chuỗi null như thể chúng là chuỗi trống.

Sử dụng phương pháp từ các bài viết trước trong loạt bài này, chúng ta có thể xác định bốn trường hợp kiểm thử unit test như sau:

  1. Các chuỗi là bằng nhau, CIEqualsOption.Normal được sử dụng.
  2. Các chuỗi KHÔNG bằng nhau, CIEqualsOption.Normal được sử dụng.
  3. Các chuỗi là bằng nhau, CIEqualsOption.NullEqualsEmpty được sử dụng.
  4. Các chuỗi KHÔNG bằng nhau, CIEqualsOption.NullEqualsEmpty được sử dụng.

Có một vấn đề ở đây. Từ bốn trường hợp này, chúng ta có thể giả định rằng chúng ta chỉ cần viết bốn bài kiểm tra unit test bằng cách sử dụng thuộc tính [Fact] của XUnit.

Nhưng hãy xem xét điều này: trong trường hợp 2, điều gì sẽ xảy ra nếu chuỗi đầu tiên trống nhưng chuỗi thứ hai là null? Chẳng phải chúng ta cũng nên kiểm tra điều gì sẽ xảy ra khi hai chuỗi giống nhau, chỉ khác một ký tự?

Trong trường hợp 1, nếu chuỗi đầu tiên là "Test" và chuỗi thứ hai là "Test 2", chuỗi đó sẽ không thành công và nó CŨNG không thành công nếu chuỗi thứ hai là null.

Có rất nhiều sự kết hợp có thể có của các bài kiểm tra unit test, và nếu chúng ta chỉ viết chúng bằng cách sử dụng thuộc tính [Fact], chúng ta sẽ kết thúc với rất nhiều mã không cần thiết.

Tại sao nó là không cần thiết? Bởi vì xUnit cung cấp một cách để thực hiện loại thử nghiệm này ngắn gọn hơn nhiều bằng cách sử dụng các thuộc tính [Theory][InlineData].

Sử dụng thuộc tính Theory và InlineData của xUnit để viết unit test

Một sự thật, trong các bài kiểm tra xUnit, theo định nghĩa là một phương pháp kiểm tra không có đầu vào. Do đó, nó được chạy như một thử nghiệm duy nhất: chuẩn bị một lần, thực hiện một lần và xác minh một lần.

Ngược lại, thuộc tính Theory trong xUnit chỉ định rằng một phương thức thử nghiệm có thể có các đầu vào và phương thức đó cần được thử nghiệm cho nhiều sự kết hợp khác nhau của các đầu vào. Cách chúng ta có được những kết hợp đầu vào đó có thể được thực hiện theo một số cách.

Cách đầu tiên và cách chúng tôi sẽ trình bày trong bài đăng này là sử dụng thuộc tính InlineData. Thuộc tính [InlineData] cho phép chúng ta chỉ định rằng một bài kiểm tra riêng biệt được chạy trên một phương thức Theory cụ thể cho từng trường hợp của [InlineData].

Chúng ta có thể có bao nhiêu thuộc tính [InlineData] trên một phương thức thử nghiệm tùy thích; ràng buộc duy nhất là các giá trị được truyền vào phương thức bởi [InlineData] phải là hằng số thời gian chạy (điều này đúng với dữ liệu được truyền bằng cách sử dụng bất kỳ thuộc tính nào, không chỉ thuộc tính này).

Hãy lấy mã kiểm tra đơn vị cho Kịch bản 1 và chia nhỏ những gì nó làm.

Kịch bản #1: Chuỗi bằng nhau, CIEqualsOption.Normal được sử dụng

[Theory]
[InlineData("Test", "tEst")]
[InlineData("TeSt CaSe", "tEsT cAsE")]
[InlineData(null, null)]
[InlineData("", "")]
public void StringExtensions_CIEquals_TrueCases(string first, string second)
{
    // Act
    var result = first.CIEquals(second);
    
    // Assert
    Assert.True(result);
}

Bản thân mã kiểm tra nó rất đơn giản: nó chỉ chạy phương thức so sánh và gọi Assert.True() để xác minh kết quả.

Trên thực tế, bài kiểm tra này sẽ được chạy bốn lần, mỗi thuộc tính [InlineData] một lần. Chúng ta có thể thấy, chỉ bằng cách xem các đầu vào trong mỗi đầu vào [InlineData], rằng thực sự mỗi đầu vào phải phân biệt chữ hoa chữ thường như nhau.

Nhưng tại sao chúng ta không viết một bài kiểm tra [Fact] duy nhất cho việc này? Bởi vì bốn trường hợp thử nghiệm đang kiểm tra một cái gì đó hơi khác nhau.

Trường hợp thử nghiệm đầu tiên và thứ hai chỉ đơn thuần là thử nghiệm chức năng cơ bản của phương thức; cụ thể là hai chuỗi với các trường hợp khác nhau nhưng các chữ cái giống nhau thì bằng nhau.

Tuy nhiên, trường hợp thứ ba kiểm tra nếu hai chuỗi null bằng nhau (thực sự là như vậy) và kiểm tra thứ tư nếu hai chuỗi rỗng bằng nhau. Mỗi trường hợp thử nghiệm là khác nhau và bao gồm một khía cạnh khác nhau của phương thức.

Sử dụng cùng logic này, chúng ta hãy viết các bài kiểm tra unit test cho Kịch bản số 2.

Kịch bản #2: Chuỗi không bằng nhau, CIEqualsOption.Normal được sử dụng

[Theory]
[InlineData("Test Case", "test case ")]
[InlineData("TeSt CaSe", "tEsT cAsE 2")]
[InlineData("Test Case", null)]
[InlineData("", "Test Case")]
public void StringExtensions_CIEquals_FalseCases(string first, string second)
{
    // Act
    var result = first.CIEquals(second);
    
    // Assert
    Assert.False(result);
}

Tương tự với kịch bản đầu tiên, bài kiểm tra ở đây đang kiểm tra nhiều khía cạnh khác nhau của phương thức kiểm tra. Chúng tôi cũng bao gồm các so sánh với null và một chuỗi trống để hoàn chỉnh.

Bây giờ chúng ta hãy viết mã thử nghiệm cho các tình huống còn lại.

Kịch bản #3: Các chuỗi bằng nhau, NullEqualsEmpty được sử dụng

[Theory]
[InlineData(null, "")]
[InlineData("", null)]
[InlineData(null, null)]
[InlineData("", "")]
public void StringExtensions_CIEquals_NullEqualsEmpty_TrueCases(string first, string second)
{
    var result = first.CIEquals(second, CIEqualsOption.NullEqualsEmpty);
    Assert.True(result);
}

Kịch bản #4: Các chuỗi không bằng nhau, sử dụng NullEqualsEmpty

[Theory]
[InlineData("Test Case", "test case ")]
[InlineData("TeSt CaSe", "tEsT cAsE 2")]
[InlineData("Test Case", null)]
[InlineData("", "Test Case")]
public void StringExtensions_CIEquals_FalseCases(string first, string second)
{
    var result = first.CIEquals(second, CIEqualsOption.NullEqualsEmpty);
    Assert.False(result);
}

Tương tự với các kịch bản trước đó, mỗi bài kiểm tra unit test đang kiểm tra các khía cạnh khác nhau của kịch bản mà chúng được viết cho.

Chúng ta có bao nhiêu bài kiểm tra unit test?

Trong xUnit, mỗi thuộc tính [InlineData] ứng với một bài kiểm tra riêng biệt. Từ các bài kiểm tra unit test ở trên, chúng ta đã viết tổng cộng bốn phương thức, với tổng cộng là 4 x 4 = 16 bài kiểm tra unit test.

Hãy thử tưởng tượng bạn đang cố gắng viết ra từng tình huống này dưới dạng một phương thức [Fact], bạn sẽ phải viết rất nhiều bài kiểm tra unit test phải không nào.

Nếu bạn muốn biết thêm lý do tại sao thuộc tính Theory và InlineData lại hữu ích như vậy, hãy tưởng tượng bạn đang cố gắng viết những bài kiểm tra này cho ngay cả một ứng dụng nhỏ sẵn sàng sản xuất. Tôi sẽ không ngạc nhiên nếu có hàng trăm hoặc hàng nghìn bài kiểm tra và việc cố gắng viết từng bài kiểm tra riêng lẻ sẽ mất rất nhiều thời gian.

[Theory][InlineData] (cùng với các thuộc tính khác là [ClassData][MemberData]) tiết kiệm rất nhiều thời gian cho các nhà phát triển khi cố gắng viết các nhóm bài kiểm tra unit test có liên quan chặt chẽ với nhau.

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ẽ hướng dẫn bạn 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

Tóm lược

Các phương thức thử nghiệm được đánh dấu bằng thuộc tính [Theory] có thể có các tham số đầu vào và có các giá trị được chuyển cho chúng bằng cách sử dụng thuộc tính [InlineData].

Bằng cách này, một phương thức thử nghiệm duy nhất có thể hỗ trợ nhiều bài kiểm tra và các nhà phát triển tiết kiệm đáng kể thời gian viết bài kiểm tra bằng cách sử dụng các thuộc tính này. Trong ví dụ của chúng tôi, chúng tôi đã viết bốn phương thức nhưng có tới 16 bài thử nghiệm sử dụng chúng.

Unit TestXUnitLập Trình C#ASP.NET Core MVC
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.

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
Trung Nguyen 18/04/2021
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ẽ hướng dẫn bạn 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

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.