停不了的风 發表於 2023-9-14 10:58:00

10分钟理解契约测试及如何在C#中实现

<p>在软件开发中,确保微服务和API的可靠性和稳定性非常重要。 随着应用程序变得越来越复杂,对强大的测试策略的需求也越来越大,这些策略可以帮助团队在不牺牲敏捷性的情况下交付高质量的代码。 近年来获得广泛关注的一种方法是契约测试(Contract Testing)。 在本文中,我将揭开契约测试的神秘面纱,并向您展示如何在 C# 项目中实现它。</p>
<h2>1.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 术语</h2>
<p><strong>消费者(</strong><strong>Consumer</strong><strong>)</strong><strong>:</strong>对服务进行消费的代码,通常指的是客户端。</p>
<p><strong>提供者(</strong><strong>Provider</strong><strong>)</strong><strong>:</strong> 提供服务的代码,通常指的是服务器端。</p>
<p><strong>契约(</strong><strong>Contract</strong><strong>)</strong><strong>:</strong> 消费者和提供者之间商定的协议。 它包括预期的请求(输入)和响应(输出)。</p>
<h2>2.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 为什么需要契约测试?</h2>
<p>构建和维护微服务是一项艰巨的任务。 在众多服务必须彼此无缝交互的世界中,确保对一项服务的更改不会破坏另一项服务的功能是很让人头疼的。 传统的集成测试针对的是整个系统之间的交互,工作量太大、速度太慢,甚至无法直接识别问题。 与之相反的是,契约测试侧重于测试各个服务之间的契约。 合同测试根据消费者和提供商之间商定的契约分别对消费者和提供商进行测试。</p>
<h2>3.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如何执行契约测试</h2>
<p>在契约测试中,消费者端程序员编写“消费者测试”,其中包含期望的输入和输出,并且期望将被保存到 Pact Json 文件中。 运行时,测试将请求发送到内置的模拟服务器而不是真实服务器,模拟服务器使用保存的 Pact Json 文件发送响应,该响应将用于验证消费者端测试用例。</p>
<p>此外,契约测试框架将读取保存的 Pact Json 文件,并向服务提供者(服务器)发送请求,并且将根据 Pact Json 文件中的预期输出来验证响应。</p>
<h2>4.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; What is Pact?</h2>
<p>Pact 是合约测试的实现。 由于消费者和提供者可能使用不同的编程语言进行开发,因此 Pact 是语言无关的,它支持多种编程语言,例如 Java、.NET、Ruby、JavaScript、Python、PHP 等。保存的 Pact Json 文件是由 用一种编程语言开发的消费者可以用来验证用另一种编程语言开发的提供者。</p>
<p>在本文中,消费者和提供者都是使用.NET (C#) 开发的。 Pact.Net 是 Pact 在 .Net 中的实现。</p>
<h2>5.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如何使用Pact.Net?</h2>
<p>使用Pact.Net总共分三步:开发一个待测试的WebAPI服务;编写消费者端测试用例;编写提供者端测试用例。</p>
<h2>a.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 开发待测试的WebAPI服务</h2>
<p>创建一个 ASP.Net Core WebAPI项目,然后如下编写一个简单的控制器。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">
/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)]
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> MyController : ControllerBase
{
   
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span> Abs(<span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)"> i)
    {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> Math.Abs(i);
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<p>上面的控制器提供了一个计算给定整数的绝对值的简单服务。</p>
<p>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”。</p>
<h2>b.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 编写消费者端测试用例</h2>
<p>创建一个使用xUnit的.NET测试项目,然后在测试项目上安装“PactNet”这个Nuget包。然后编写如下的测试用例。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> UnitTest1
{
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span><span style="color: rgba(0, 0, 0, 1)"> IPactBuilderV4 pactBuilder;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> UnitTest1()
    {
      </span><span style="color: rgba(0, 0, 255, 1)">var</span> pact = Pact.V4(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">MyAPI consumer</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">MyAPI</span><span style="color: rgba(128, 0, 0, 1)">"</span>,<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> PactConfig());
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pactBuilder =<span style="color: rgba(0, 0, 0, 1)"> pact.WithHttpInteractions();
    }
   
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">async</span><span style="color: rgba(0, 0, 0, 1)"> Task Test1()
    {
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.pactBuilder.UponReceiving(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">A request to calc Abs</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .Given(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Abs</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .WithRequest(HttpMethod.Get, </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/My/Abs</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .WithQuery(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">i</span><span style="color: rgba(128, 0, 0, 1)">"</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">-2</span><span style="color: rgba(128, 0, 0, 1)">"</span>)<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Match.Integer(-2) </span>
<span style="color: rgba(0, 0, 0, 1)">            .WillRespond()
            .WithStatus(HttpStatusCode.OK)
            .WithJsonBody(</span><span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">);

      </span><span style="color: rgba(0, 0, 255, 1)">await</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.pactBuilder.VerifyAsync(<span style="color: rgba(0, 0, 255, 1)">async</span> ctx=&gt;<span style="color: rgba(0, 0, 0, 1)">
      {
            </span><span style="color: rgba(0, 0, 255, 1)">using</span> HttpClient httpClient = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpClient();
            httpClient.BaseAddress </span>=<span style="color: rgba(0, 0, 0, 1)"> ctx.MockServerUri;
            </span><span style="color: rgba(0, 0, 255, 1)">var</span> r = <span style="color: rgba(0, 0, 255, 1)">await</span> httpClient.GetFromJsonAsync&lt;<span style="color: rgba(0, 0, 255, 1)">int</span>&gt;($<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/My/Abs?i=-2</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            Assert.Equal(</span><span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">,r);
      });
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<p>“WithRequest().WithQuery()”用于定义输入,“WillRespond().WithJsonBody()”用于定义相应的预期输出。VerifyAsync中的代码片段是测试用例,根据“UponReceiving”定义的期望进行测试。 从“httpClient.BaseAddress = ctx.MockServerUri”可以看出,Provider 测试用例与 Pact 提供的Mock服务器交互而不是真实服务器进行交互。</p>
<p>接下来,让我们运行测试,测试运行完成后,测试项目的pact文件夹下会生成一个“MyAPI Consumer-MyAPI.json”,这个Json文件中保存了预期的输入和输出,如下图。</p>
<p>&nbsp;<img src="https://img2023.cnblogs.com/blog/130406/202309/130406-20230914105810886-1103428605.png" alt=""></p>
<h2>c.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 编写提供者端测试用例</h2>
<p>创建一个使用xUnit的.NET测试项目,然后向其安装 Nuget 包“PactNet”和“PactNet.Output.Xunit”。 由于提供程序测试必须使用 Startup 类启动测试服务器,因此请将待测试的 ASP.NET Core WebAPI 项目的引用添加到提供程序测试项目中。</p>
<p>创建一个“MyApiFixture”类,用于启动测试项目中测试的WebAPI服务器。MyApiFixture类的代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> MyApiFixture: IDisposable
{
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span><span style="color: rgba(0, 0, 0, 1)"> IHost server;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> Uri ServerUri { <span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">; }
    </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> MyApiFixture()
    {
      ServerUri </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Uri(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">http://localhost:9223</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      server </span>=<span style="color: rgba(0, 0, 0, 1)"> Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webBuilder </span>=&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                webBuilder.UseUrls(ServerUri.ToString());
                webBuilder.UseStartup</span>&lt;Startup&gt;<span style="color: rgba(0, 0, 0, 1)">();
            })
            .Build();
      server.Start();
    }

    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Dispose()
    {
      server.Dispose();
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<p>接下来,如下创建一个使用保存的Pact Json文件对服务器(提供者)进行测试的测试用例。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span> MyApiTest1: IClassFixture&lt;MyApiFixture&gt;<span style="color: rgba(0, 0, 0, 1)">
{
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span><span style="color: rgba(0, 0, 0, 1)"> MyApiFixture fixture;
    </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span><span style="color: rgba(0, 0, 0, 1)"> ITestOutputHelper output;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> MyApiTest1(MyApiFixture fixture,ITestOutputHelper output)
    {
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.fixture =<span style="color: rgba(0, 0, 0, 1)"> fixture;
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.output =<span style="color: rgba(0, 0, 0, 1)"> output;
    }
   
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">async</span><span style="color: rgba(0, 0, 0, 1)"> Task Test1()
    {
      </span><span style="color: rgba(0, 0, 255, 1)">var</span> config = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> PactVerifierConfig
      {
            Outputters </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> List&lt;IOutput&gt;<span style="color: rgba(0, 0, 0, 1)">
            {
                </span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> XunitOutput(output),
            },
      };
      </span><span style="color: rgba(0, 0, 255, 1)">string</span> pactPath = Path.Combine(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">..</span><span style="color: rgba(128, 0, 0, 1)">"</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">..</span><span style="color: rgba(128, 0, 0, 1)">"</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">..</span><span style="color: rgba(128, 0, 0, 1)">"</span>,<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">..</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
            </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">TestConsumerProject1</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">pacts</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">MyAPI consumer-MyAPI.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      </span><span style="color: rgba(0, 0, 255, 1)">using</span> <span style="color: rgba(0, 0, 255, 1)">var</span> pactVerifier = <span style="color: rgba(0, 0, 255, 1)">new</span> PactVerifier(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">MyAPI</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">, config);
      pactVerifier
            .WithHttpEndpoint(fixture.ServerUri)
            .WithFileSource(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> FileInfo(pactPath))
            .Verify();
    }
}</span></pre>
</div>
<p>&nbsp;</p>
<p>“pactPath”是指保存的Pact文件,在您的项目中,它会根据项目名称、相对路径的不同而不同。 执行上述测试时,Pact 将启动测试项目中的测试服务器,发送请求并根据保存的 Json 文件验证响应。</p>
<h2>6.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 对基于消息的服务使用Pact</h2>
<p>Pact也支持对于基于消息的服务(也被称为async API)进行测试。详细请查看Pact文档的“messaging pacts”部分。</p><br><br>
来源:https://www.cnblogs.com/rupeng/p/17701948.html
頁: [1]
查看完整版本: 10分钟理解契约测试及如何在C#中实现