自動化測試資料生成:Asp.Net Core單元測試利器AutoFixture詳解

董瑞鹏發表於2024-04-29

引言

在我們之前的文章中介紹過使用Bogus生成模擬測試資料,今天來講解一下功能更加強大自動生成測試資料的工具的庫"AutoFixture"

什麼是AutoFixture?

AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高可維護性。它的主要目標是讓開發人員專注於被測試的內容,而不是如何設定測試場景,透過更容易地建立包含測試資料的物件圖,從而實現這一目標。

AutoFixture 可以幫助開發人員自動生成測試資料,減少手動設定測試資料的工作量,提高單元測試的效率和可維護性。透過自動生成物件,開發人員可以更專注於編寫測試邏輯,而不必花費大量精力在準備測試資料上。

其實和Bogus相比,AutoFixture更強大的地方在於可以自動化設定物件的值,當類發生變化時如屬性名或者型別更改,我們不需要去進行維護,AutoFixture可以自動適應Class的變化。

AutoFixture與流行的 .NET 測試框架(如 NUnitxUnit)可以無縫整合。

AutoFixture實戰

我們在建立xUnit單元測試專案dotNetParadise.AutoFixture

安裝依賴

建立完專案之後我們首先要安裝Nuget

PM> NuGet\Install-Package AutoFixture -Version 4.18.1

初始化

AutoFixture的使用是從一個Fixture的例項物件開始的

var fixture = new Fixture();

接下來我們先建立一個測試類來學一下AutoFixture的使用

public class AutoFixtureStaffTest
{
    private readonly IFixture _fixture;
    public AutoFixtureStaffTest()
    {
        _fixture = new Fixture();
    }
}

實戰

我們之前的測試專案建立了Sample.ApiSample.Repository兩個類庫來做我們被測試的專案,本章繼續使用Sample.Repository來演示AutoFixture的使用。

dotNetParadise.AutoFixture 測試專案新增Sample.Repository的專案引用

Sample.Repository中我們有一個Staff的實體物件,繼續用作我們的測試

public class Staff
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
    public List<string>? Addresses { get; set; }
    public DateTimeOffset? Created { get; set; }
    public void Update(Staff staff)
    {
        this.Name = staff.Name;
        this.Email = staff.Email;
        this.Age = staff.Age;
        this.Addresses = staff.Addresses;
        Created = staff.Created;
    }
}

屬性賦值

   [Fact]
   public void Staff_SetProperties_ValuesAssignedCorrectly()
   {
       //Arrange
       Staff staff = new Staff();
       //生成Int型別
       staff.Id = _fixture.Create<int>();
       //生成string 型別
       staff.Name = _fixture.Create<string>();
       //生成DateTimeOffset型別
       staff.Created = _fixture.Create<DateTimeOffset>();
       //生成 List<string>?
       staff.Addresses = _fixture.CreateMany<string>(Random.Shared.Next(1, 100)).ToList();
       //Act
       //...省略
       // Assert
       Assert.NotNull(staff); // 驗證 staff 物件不為 null

       // 驗證 staff.Id 是 int 型別
       Assert.IsType<int>(staff.Id);

       // 驗證 staff.Name 是 string 型別
       Assert.IsType<string>(staff.Name);

       // 驗證 staff.Created 是 DateTimeOffset? 型別
       Assert.IsType<DateTimeOffset>(staff.Created);

       // 驗證 staff.Addresses 是 List<string> 型別
       Assert.IsType<List<string>>(staff.Addresses);

       // 驗證 staff.Addresses 不為 null
       Assert.NotNull(staff.Addresses);

       // 驗證 staff.Addresses 中的元素數量在 1 到 100 之間
       Assert.InRange(staff.Addresses.Count, 1, 100);
   }

示例中用到 AutoFixture 提供的的方法隨機分配隨機值,上面的示例中用到使用到了兩個方法

Create<T>方法

  • 用於生成一個指定型別 T 的例項。它會自動填充物件的屬性和欄位,以便建立一個完整的物件例項。
  • 這個方法通常用於生成單個物件例項,適用於需要單個物件作為測試資料的情況。
  • 當呼叫 Create<T> 方法時,AutoFixture 會根據 T 型別的建構函式、屬性和欄位來自動生成合適的值,以確保物件例項的完整性和一致性。

CreateMany<T>方法

  • 用於生成多個指定型別 T 的例項,通常用於生成集合或列表型別的測試資料。
  • 這個方法允許你指定要生成的例項數量,並返回一個包含這些例項的 IEnumerable 集合。
  • 當呼叫 CreateMany<T> 方法時,AutoFixture 會根據 T 型別的建構函式、屬性和欄位來生成指定數量的物件例項,以便填充集合或列表。

T包括基本型別(如 stringint)、自定義物件等

Create<T>構造物件

上面的例子我們自己例項化的物件然後對物件挨個賦值,目的是讓大家對AutoFixture的使用有一個初步的認識,上面也解釋到了Create<T>的泛型引數T可以是自定義的物件,那麼我們來簡化一下上面的示例

[Fact]
public void Staff_ObjectCreation_ValuesAssignedCorrectly()
{
    // Arrange
    Staff staff = _fixture.Create<Staff>(); // 使用 AutoFixture 直接建立 Staff 物件

    // Act
    //...省略

    // Assert
    Assert.NotNull(staff); // 驗證 staff 物件不為 null

    // 驗證 staff.Id 是 int 型別
    Assert.IsType<int>(staff.Id);

    // 驗證 staff.Name 是 string 型別
    Assert.IsType<string>(staff.Name);

    // 驗證 staff.Created 是 DateTimeOffset? 型別
    Assert.IsType<DateTimeOffset>(staff.Created);

    // 驗證 staff.Addresses 是 List<string> 型別
    Assert.IsType<List<string>>(staff.Addresses);

    // 驗證 staff.Addresses 不為 null
    Assert.NotNull(staff.Addresses);

    // 驗證 staff.Addresses 中的元素數量在 1 到 100 之間
    Assert.InRange(staff.Addresses.Count, 1, 100);
}

修改後的例子中,我們使用 AutoFixtureCreate<Staff>() 方法直接建立了一個 Staff 物件,而不是手動為每個屬性賦值。這樣可以更簡潔地生成物件例項。

資料驅動測試

在正常的同一個測試方法中使用不同的輸入資料進行測試時,通常都是基於 Theory 屬性配合InlineData或者MemberData來完成的,有了AutoFixture之後資料也不用我們自己造了,來看一下實戰入門

第一步Nuget安裝依賴

PM> NuGet\Install-Package AutoFixture.Xunit2 -Version 4.18.1

[AutoData]屬性

[Theory, AutoData]
public void Staff_Constructor_InitializesPropertiesCorrectly(
    int id, string name, string email, int? age, List<string> addresses, DateTimeOffset? created)
{
    // Act
    var staff = new Staff { Id = id, Name = name, Email = email, Age = age, Addresses = addresses, Created = created };

    // Assert
    Assert.Equal(id, staff.Id);
    Assert.Equal(name, staff.Name);
    Assert.Equal(email, staff.Email);
    Assert.Equal(age, staff.Age);
    Assert.Equal(addresses, staff.Addresses);
    Assert.Equal(created, staff.Created);
}

透過 AutoData 方法,測試方法的引數化設定變得更加簡單和高效,使得編寫引數化測試方法變得更加容易。

[InlineAutoData]屬性

如果我們有需要提供的特定化引數,可以用[InlineAutoData]屬性,具體使用可以參考如下案例

    [Theory]
    [InlineAutoData(1)]
    [InlineAutoData(2)]
    [InlineAutoData(3)]
    [InlineAutoData]
    public void Staff_ConstructorByInlineData_InitializesPropertiesCorrectly(
     int id, string name, string email, int? age, List<string> addresses, DateTimeOffset? created)
    {
        // Act
        var staff = new Staff { Id = id, Name = name, Email = email, Age = age, Addresses = addresses, Created = created };

        // Assert
        Assert.Equal(id, staff.Id);
        Assert.Equal(name, staff.Name);
        Assert.Equal(email, staff.Email);
        Assert.Equal(age, staff.Age);
        Assert.Equal(addresses, staff.Addresses);
        Assert.Equal(created, staff.Created);
    }

自定義物件屬性值

AutoFixtureBuild 方法結合 With 方法可以用於自定義物件的屬性值

    [Fact]
    public void Staff_SetCustomValue_ShouldCorrectly()
    {
        var staff = _fixture.Build<Staff>()
            .With(_ => _.Name, "Ruipeng")
            .Create();
        Assert.Equal("Ruipeng", staff.Name);
    }

禁用屬性自動生成

AutoFixture 中,可以使用 OmitAutoProperties 方法來關閉自動屬性生成,從而避免自動生成屬性值。這在需要手動設定所有屬性值的情況下很有用。

    [Fact]
    public void Test_DisableAutoProperties()
    {
        // Arrange
        var fixture = new Fixture();
        var sut = fixture.Build<Staff>()
                         .OmitAutoProperties()
                         .Create();

        // Assert
        Assert.Equal(0, sut.Id); // 驗證 Id 屬性為預設值 0
        Assert.Null(sut.Name); // 驗證 Name 屬性為 null
        Assert.Null(sut.Email); // 驗證 Email 屬性為 null
        Assert.Null(sut.Age); // 驗證 Age 屬性為 null
        Assert.Null(sut.Addresses); // 驗證 Addresses 屬性為 null
        Assert.Null(sut.Created); // 驗證 Created 屬性為 null
    }

Do 方法執行自定義操作

Do 方法是 AutoFixture 中用於執行操作的方法,通常結合 Build 方法一起使用,用於在構建物件時執行自定義操作。讓我詳細解釋一下 Do 方法的用法和作用:

主要特點:

  • 執行操作:Do 方法允許在物件構建過程中執行自定義操作,例如向集合新增元素、設定屬性值等。
  • 鏈式呼叫:可以透過鏈式呼叫多個 Do 方法,依次執行多個操作。
  • 靈活定製:透過 Do 方法,可以在物件構建過程中靈活地定製物件的屬性值或執行其他操作。
    使用方法:
  • 結合 Build 方法:通常與 Build 方法一起使用,用於為物件構建器執行操作。
  • 執行自定義操作:在 Do 方法中傳入一個 lambda 表示式,可以在 lambda 表示式中執行需要的操作。
  • 鏈式呼叫:可以多次呼叫 Do 方法,實現多個操作的順序執行。
   [Fact]
   public void Test_UpdateMethod()
   {
       // Arrange
       var fixture = new Fixture();
       var staff1 = fixture.Create<Staff>();
       var staff2 = fixture.Create<Staff>();

       // 使用 Do 方法執行自定義操作
       var staff3 = fixture.Build<Staff>()
                                 .Do(x => staff1.Update(staff2))
                                 .Create();

       // Assert
       Assert.Equal(staff2.Name, staff1.Name); // 驗證 Name 是否更新
       Assert.Equal(staff2.Email, staff1.Email); // 驗證 Email 是否更新
       Assert.Equal(staff2.Age, staff1.Age); // 驗證 Age 是否更新
       Assert.Equal(staff2.Addresses, staff1.Addresses); // 驗證 Addresses 是否更新
       Assert.Equal(staff2.Created, staff1.Created); // 驗證 Created 是否更新
   }

建立三個物件,在第三個建立過程中把第一個的物件屬性用第二個物件的屬性覆蓋。

Customize Type 自定義型別

使用自定義型別構建器來執行復雜的初始化,並且保證了建立多個相同的例項中要保持一致的自定義行為。

首先我們可以在我們的測試類建構函式中定義一個自定義規則

    public AutoFixtureStaffTest()
    {
        _fixture = new Fixture();
        _fixture.Customize<Staff>(composer => composer.With(x => x.Email, "zhangsan@163.com"));
    }

比如我設定了所有的 email 都叫zhangsan@163.com

    [Fact]
    public void Test_StaffNameIsJohnDoe()
    {
        // Arrange
        Staff staff = _fixture.Create<Staff>();

        // Act

        // Assert
        Assert.Equal("zhangsan@163.com", staff.Email);
    }

這個位置大概得思想就是這樣,保證用到的多例項都有相同的行為,可以參考:
使用 AutoFixture 自定義型別的生成器

Auto-Mocking with Moq

第一步安裝Nuget

PM> NuGet\Install-Package AutoFixture.AutoMoq -Version 4.18.1

    [Fact]
    public async Task Repository_Add_ShouleBeSuccess()
    {
        _fixture.Customize(new AutoMoqCustomization());
        var repoMock = _fixture.Create<IStaffRepository>();
        Assert.NotNull(repoMock);
    }

建立 Fixture 例項並使用 AutoMoqCustomization 進行定製化,以便自動模擬 Moq 物件。
使用 Create<IInterface>() 方法建立一個可分配給 IInterface 介面的模擬例項。

Auto-configured Mocks

官網示例:

fixture.Customize(new AutoMoqCustomization { ConfigureMembers = true });
fixture.Inject<int>(1234);

var document = fixture.Create<IDocument>();
Console.WriteLine(document.Id); // 1234

當將 ConfigureMembers = true 新增到 AutoMoqCustomization 中時,不僅會作為自動模擬容器,還會自動配置所有生成的模擬物件,使其成員返回 AutoFixture 生成的值。
使用 Inject<int>(1234) 將整數值 1234注入到 Fixture 中。
使用 Create<IDocument>() 建立一個 IDocument 介面的例項,並輸出其 Id 屬性值。

更多

Moq 框架中存在一些限制,其中自動配置模式無法設定具有 ref引數的方法,並且也無法配置泛型方法。然而,您可以使用 ReturnsUsingFixture 擴充套件方法輕鬆地設定這些方法。

官網示例:

converter.Setup(x => x.Convert<double>("10.0"))
         .ReturnsUsingFixture(fixture);

在這個示例中,使用 ReturnsUsingFixture 擴充套件方法手動設定了一個名為 Convert 的泛型方法的行為。
當呼叫 Convert 方法並傳入字串"10.0" 時,ReturnsUsingFixture方法將使用fixture生成的值作為返回值。 透過使用ReturnsUsingFixture擴充套件方法,您可以繞過Moq框架的限制,手動設定具有ref` 引數或泛型方法的行為,以滿足特定的測試需求.

最後

AutoFixture就像是一個自動資料生成器,讓我們的單元測試變得更簡單、更高效。透過使用它,我們可以輕鬆地建立測試資料,專注於寫好測試邏輯,而不用為資料準備的瑣事煩惱.

  • AutoFixture
  • 本文完整原始碼

😄歡迎關注筆者公眾號一起學習交流,獲取更多有用的知識~
image

相關文章