WireMock.NET如何帮助进行.NET Core应用程序的集成测试

目录

介绍

背景

使用代码

兴趣点


介绍

如果您是执行TDDASP.NET Core开发人员,您可能会遇到一些问题。您的测试不涵盖您的Program类和Startup类。您的模拟框架有助于模拟内部依赖项,但不能模拟外部依赖项,例如其他公司制作的Web服务。此外,也许您决定不测试某些类,因为要模拟的内部依赖项太多。在本文中,我将解释如何解决这些问题。

背景

如果您对.NET Core 3.1(我在这里使用的版本)的TDD有一些经验,这将很有帮助,最好使用xUnit

使用代码

首先,让我们实现该ConfigureServices方法。我们依赖于appsettings.json文件中设置的外部服务和依赖于HttpClient.

添加了重试策略以确保在意外失败时重试请求。

public void ConfigureServices(IServiceCollection services)
{
      services.AddControllers();
      var googleLocation = Configuration["Google"];
      services.AddHttpClient<ISearchEngineService, SearchEngineService>(c =>
           c.BaseAddress = new Uri(googleLocation))
           .SetHandlerLifetime(TimeSpan.FromMinutes(5))
           .AddPolicyHandler(GetRetryPolicy());          
}       

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
     return HttpPolicyExtensions
       .HandleTransientHttpError().OrTransientHttpStatusCode()
       .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

此外,还需要实现这个类来实例化以进行依赖注入(到控制器中)。只有一种方法。它调用外部服务并返回字符数。

public class SearchEngineService : ISearchEngineService
{
   private readonly HttpClient _httpClient;
   public SearchEngineService(HttpClient httpClient)
   {
        _httpClient = httpClient;
   }
    
   public async Task<int> GetNumberOfCharactersFromSearchQuery(string toSearchFor)
   {
        var result = await _httpClient.GetAsync($"/search?q={toSearchFor}");             
        var content = await result.Content.ReadAsStringAsync();
        return content.Length;
   }
}

从逻辑上讲,我们也需要实现控制器。

[Route("api/[controller]")]
[ApiController]
public class SearchEngineController : ControllerBase
{
     private readonly ISearchEngineService _searchEngineService;

     public SearchEngineController(ISearchEngineService searchEngineService)
     {
          _searchEngineService = searchEngineService;
     }

     [HttpGet("{queryEntry}", Name = "GetNumberOfCharacters")]
     public async Task<ActionResult<int>> GetNumberOfCharacters(string queryEntry)
     {
         var numberOfCharacters = 
             await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry);
         return Ok(numberOfCharacters);
     }
}

要使用来自自动化测试的Web请求测试所有内容,我们需要对Web应用程序进行自托管(在xUnit测试期间)。为此,我们需要在基类中使用WebApplicationFactory

public abstract class TestBase : IDisposable, IClassFixture<WebApplicationFactory<Startup>>
{
    protected readonly HttpClient HttpClient;

    public TestBase(WebApplicationFactory<Startup> factory, int portNumber, bool useHttps)
    {
        var extraConfiguration = GetConfiguration();
        string afterHttp = useHttps ? "s" : "";
        HttpClient = factory.WithWebHostBuilder(whb =>
        {
            whb.ConfigureAppConfiguration((context, configbuilder) =>
            {
                configbuilder.AddInMemoryCollection(extraConfiguration);
            });
        }).CreateClient(new WebApplicationFactoryClientOptions
        {
            BaseAddress = new Uri($"http{afterHttp}://localhost:{portNumber}")
        });
    }

    protected virtual Dictionary<string, string> GetConfiguration()
    {
       return new Dictionary<string, string>();
    }

    protected virtual void Dispose(bool disposing)
    {
       if (disposing)
       {
          HttpClient.Dispose();
       }
    }

    public void Dispose()
    {
       Dispose(true);
       GC.SuppressFinalize(this);
    }
}

这个基类做了以下事情:

  • 创建一个HttpClient对我们自己的应用程序进行REST调用而不启动它(由CreateClient完成)
  • 运行StartupProgram类中的代码(也由CreateClient完成)
  • 使用AddInMemoryCollection专门为我们的测试更新配置 
  • 每次测试后释放HttpClient

现在我们有了基类,我们可以实现实际的测试。

public class SearchEngineClientTest : TestBase
{
   private FluentMockServer _mockServerSearchEngine;

   public SearchEngineClientTest(WebApplicationFactory<Startup> factory) : 
                                 base(factory, 5347, false)
   {
   }

   [Theory]
   [InlineData("Daan","SomeResponseFromGoogle")]
   [InlineData("Sean","SomeOtherResponseFromGoogle")]
   public async Task TestWithStableServer(string searchQuery, string externalResponseContent)
   {
        SetupStableServer(externalResponseContent);
        var response = await HttpClient.GetAsync($"/api/searchengine/{searchQuery}");
        response.EnsureSuccessStatusCode();
        var actualResponseContent = await response.Content.ReadAsStringAsync();
        Assert.Equal($"{externalResponseContent.Length}", actualResponseContent);
        var requests = 
               _mockServerSearchEngine.LogEntries.Select(l => l.RequestMessage).ToList();
        Assert.Single(requests);
        Assert.Contains($"/search?q={searchQuery}", requests.Single().AbsoluteUrl);
   }

   [Theory]
   [InlineData("Daan", "SomeResponseFromGoogle")]
   [InlineData("Sean", "SomeOtherResponseFromGoogle")]
   public async Task TestWithUnstableServer
                (string searchQuery, string externalResponseContent)
   {
        SetupUnStableServer(externalResponseContent);
        var response = await HttpClient.GetAsync($"/api/searchengine/{searchQuery}");
        response.EnsureSuccessStatusCode();
        var actualResponseContent = await response.Content.ReadAsStringAsync();
        Assert.Equal($"{externalResponseContent.Length}", actualResponseContent);
        var requests = 
           _mockServerSearchEngine.LogEntries.Select(l => l.RequestMessage).ToList();
        Assert.Equal(2,requests.Count);
        Assert.Contains($"/search?q={searchQuery}", requests.Last().AbsoluteUrl);
        Assert.Contains($"/search?q={searchQuery}", requests.First().AbsoluteUrl);
   }

   protected override Dictionary<string, string> GetConfiguration()
   {
        _mockServerSearchEngine = FluentMockServer.Start();
        var googleUrl = _mockServerSearchEngine.Urls.Single();
        var configuration = base.GetConfiguration();
        configuration.Add("Google", googleUrl);
        return configuration;
   }

   protected override void Dispose(bool disposing)
   {
        base.Dispose(disposing);
        if (disposing)
        {
            _mockServerSearchEngine.Stop();
            _mockServerSearchEngine.Dispose();
        }
   }

   private void SetupStableServer(string response)
   {
        _mockServerSearchEngine.Given(Request.Create().UsingGet())
             .RespondWith(Response.Create().WithBody(response, encoding:Encoding.UTF8)
             .WithStatusCode(HttpStatusCode.OK));
   }

   private void SetupUnStableServer(string response)
   {
       _mockServerSearchEngine.Given(Request.Create().UsingGet())
             .InScenario("UnstableServer")
             .WillSetStateTo("FIRSTCALLDONE")
             .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
             .WithStatusCode(HttpStatusCode.InternalServerError));

       _mockServerSearchEngine.Given(Request.Create().UsingGet())
             .InScenario("UnstableServer")
             .WhenStateIs("FIRSTCALLDONE")
             .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
             .WithStatusCode(HttpStatusCode.OK));
    }
}

Web应用程序和外部服务都是自托管的。无需启动其中之一。我们像进行单元测试一样进行测试。这就是方法的作用:

  • SetupStableServer:我们设置了一个模拟的外部服务,并确保它的行为像一个稳定的服务。它总是返回状态码为200的响应。
  • SetupUnStableServer:这是设置一个模拟的外部服务,在第一个请求失败后返回 200500,内部服务器错误)
  • Dispose: 停止对外服务
  • GetConfiguration:返回新的配置设置。我们使用具有不同 (localhost) url的模拟外部服务。
  • TestWithStableServer: 用稳定的服务器测试。我们调用我们自己的服务并验证我们自己的服务发送的请求(它必须是一个)是正确的。
  • TestWithUnstableServer:一种非常相似的方法,但由于外部服务的行为不稳定,因此预计会发送两个请求,我们有重试策略来处理这种情况。

兴趣点

有关于.NET Core集成测试的很好的文档。还有很好的文档关于WireMock.NET。我刚刚解释了如何结合这些技术,这确实是一个不同且被低估的主题。集成测试是实现良好代码覆盖率的一种非常好的方式,通过REST调用测试应用程序而无需托管和部署,并使测试真实,因为不需要模拟内部依赖项。但是,仍然需要模拟外部依赖项。否则,测试失败并不意味着你自己的应用程序(外部应用程序可能宕机),成功也不意味着这么多(它可能无法处理外部服务的意外故障)。因此,WireMock.NET可以帮助您。它使您的测试更有意义。

如果您对完整的源代码感兴趣,可以在GitHub找到

https://www.codeproject.com/Articles/5267354/How-WireMock-NET-Can-Help-in-Doing-Integration-Tes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值