10分鐘理解契約測試及如何在C#中實現

2023-09-14 12:00:21

在軟體開發中,確保微服務和API的可靠性和穩定性非常重要。 隨著應用程式變得越來越複雜,對強大的測試策略的需求也越來越大,這些策略可以幫助團隊在不犧牲敏捷性的情況下交付高質量的程式碼。 近年來獲得廣泛關注的一種方法是契約測試(Contract Testing)。 在本文中,我將揭開契約測試的神祕面紗,並向您展示如何在 C# 專案中實現它。

1.      術語

消費者(Consumer:對服務進行消費的程式碼,通常指的是使用者端。

提供者(Provider: 提供服務的程式碼,通常指的是伺服器端。

契約(Contract: 消費者和提供者之間商定的協定。 它包括預期的請求(輸入)和響應(輸出)。

2.      為什麼需要契約測試?

構建和維護微服務是一項艱鉅的任務。 在眾多服務必須彼此無縫互動的世界中,確保對一項服務的更改不會破壞另一項服務的功能是很讓人頭疼的。 傳統的整合測試針對的是整個系統之間的互動,工作量太大、速度太慢,甚至無法直接識別問題。 與之相反的是,契約測試側重於測試各個服務之間的契約。 合同測試根據消費者和提供商之間商定的契約分別對消費者和提供商進行測試。

3.      如何執行契約測試

在契約測試中,消費者端程式設計師編寫「消費者測試」,其中包含期望的輸入和輸出,並且期望將被儲存到 Pact Json 檔案中。 執行時,測試將請求傳送到內建的模擬伺服器而不是真實伺服器,模擬伺服器使用儲存的 Pact Json 檔案傳送響應,該響應將用於驗證消費者端測試用例。

此外,契約測試框架將讀取儲存的 Pact Json 檔案,並向服務提供者(伺服器)傳送請求,並且將根據 Pact Json 檔案中的預期輸出來驗證響應。

4.      What is Pact?

Pact 是合約測試的實現。 由於消費者和提供者可能使用不同的程式語言進行開發,因此 Pact 是語言無關的,它支援多種程式語言,例如 Java、.NET、Ruby、JavaScript、Python、PHP 等。儲存的 Pact Json 檔案是由 用一種程式語言開發的消費者可以用來驗證用另一種程式語言開發的提供者。

在本文中,消費者和提供者都是使用.NET (C#) 開發的。 Pact.Net 是 Pact 在 .Net 中的實現。

5.      如何使用Pact.Net?

使用Pact.Net總共分三步:開發一個待測試的WebAPI服務;編寫消費者端測試用例;編寫提供者端測試用例。

a.      開發待測試的WebAPI服務

建立一個 ASP.Net Core WebAPI專案,然後如下編寫一個簡單的控制器。

[ApiController]
[Route("[controller]/[action]")]
public class MyController : ControllerBase
{
    [HttpGet]
    public int Abs(int i)
    {
        return Math.Abs(i);
    }
}

 

上面的控制器提供了一個計算給定整數的絕對值的簡單服務。

Pact需要使用ASP.Net Core專案的Startup類來啟動Web伺服器,但是,在最新的.NET Core中,傳統的Startup.cs被Minimal API取代。如果 要將 Pact 與 .NET Core 一起使用,您必須切換到 傳統Startup 風格的程式碼,如果您不知道如何切換回傳統的Startup 風格的程式碼,請 搜尋「Adding Back the Startup Class to ASP.NET Core」。

b.      編寫消費者端測試用例

建立一個使用xUnit的.NET測試專案,然後在測試專案上安裝「PactNet」這個Nuget包。然後編寫如下的測試用例。

public class UnitTest1
{
    private readonly IPactBuilderV4 pactBuilder;
    public UnitTest1()
    {
        var pact = Pact.V4("MyAPI consumer", "MyAPI",new PactConfig());
        this.pactBuilder = pact.WithHttpInteractions();
    }
    [Fact]
    public async Task Test1()
    {
        this.pactBuilder.UponReceiving("A request to calc Abs")
            .Given("Abs")
            .WithRequest(HttpMethod.Get, "/My/Abs")
            .WithQuery("i","-2")//Match.Integer(-2) 
            .WillRespond()
            .WithStatus(HttpStatusCode.OK)
            .WithJsonBody(2);

        await this.pactBuilder.VerifyAsync(async ctx=>
        {
            using HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = ctx.MockServerUri;
            var r = await httpClient.GetFromJsonAsync<int>($"/My/Abs?i=-2");
            Assert.Equal(2,r);
        });
    }
}

 

「WithRequest().WithQuery()」用於定義輸入,「WillRespond().WithJsonBody()」用於定義相應的預期輸出。VerifyAsync中的程式碼片段是測試用例,根據「UponReceiving」定義的期望進行測試。 從「httpClient.BaseAddress = ctx.MockServerUri」可以看出,Provider 測試用例與 Pact 提供的Mock伺服器互動而不是真實伺服器進行互動。

接下來,讓我們執行測試,測試執行完成後,測試專案的pact資料夾下會生成一個「MyAPI Consumer-MyAPI.json」,這個Json檔案中儲存了預期的輸入和輸出,如下圖。

 

c.      編寫提供者端測試用例

建立一個使用xUnit的.NET測試專案,然後向其安裝 Nuget 包「PactNet」和「PactNet.Output.Xunit」。 由於提供程式測試必須使用 Startup 類啟動測試伺服器,因此請將待測試的 ASP.NET Core WebAPI 專案的參照新增到提供程式測試專案中。

建立一個「MyApiFixture」類,用於啟動測試專案中測試的WebAPI伺服器。MyApiFixture類的程式碼如下:

public class MyApiFixture: IDisposable
{
    private readonly IHost server;
    public Uri ServerUri { get; }
    public MyApiFixture()
    {
        ServerUri = new Uri("http://localhost:9223");
        server = Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseUrls(ServerUri.ToString());
                webBuilder.UseStartup<Startup>();
            })
            .Build();
        server.Start();
    }

    public void Dispose()
    {
        server.Dispose();
    }
}

 

接下來,如下建立一個使用儲存的Pact Json檔案對伺服器(提供者)進行測試的測試用例。

public class MyApiTest1: IClassFixture<MyApiFixture>
{
    private readonly MyApiFixture fixture;
    private readonly ITestOutputHelper output;
    public MyApiTest1(MyApiFixture fixture,ITestOutputHelper output)
    {
        this.fixture = fixture;
        this.output = output;
    }
    [Fact]
    public async Task Test1()
    {
        var config = new PactVerifierConfig
        {
            Outputters = new List<IOutput>
            {
                new XunitOutput(output),
            },
        };
        string pactPath = Path.Combine("..","..","..","..",
            "TestConsumerProject1", "pacts", "MyAPI consumer-MyAPI.json");
        using var pactVerifier = new PactVerifier("MyAPI", config);
        pactVerifier
            .WithHttpEndpoint(fixture.ServerUri)
            .WithFileSource(new FileInfo(pactPath))
            .Verify();
    }
}

 

「pactPath」是指儲存的Pact檔案,在您的專案中,它會根據專案名稱、相對路徑的不同而不同。 執行上述測試時,Pact 將啟動測試專案中的測試伺服器,傳送請求並根據儲存的 Json 檔案驗證響應。

6.      對基於訊息的服務使用Pact

Pact也支援對於基於訊息的服務(也被稱為async API)進行測試。詳細請檢視Pact檔案的「messaging pacts」部分。