WireMock.NET如何幫助進行.NET Core應用程序的集成測試

目錄

介紹

背景

使用代碼

興趣點


許多應用程序僅使用xUnit進行了單元測試,而未進行集成測試。.NET Core提供了進行集成測試的好方法。您的測試將比單元測試更加實際,因爲將僅模擬外部依賴關係而不會模擬內部依賴關係。WireMock.NET提供了執行此操作的方法。

介紹

如果您是執行TDDASP.NET Core開發人員,則可能會遇到一些問題。您的Program類和Startup類不在您的測試範圍內。您的模擬框架有助於模擬內部依賴關係,但不會對外部依賴項進行同樣的模擬,例如其他公司創建的web服務。而且,也許您決定不測試某些類,因爲內部依賴太多,無法模擬。在本文中,我將解釋如何解決這些問題。

背景

如果您對.NET Core 3.1TDD(我在此使用的版本)有一些經驗,最好對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:返回新的配置設置。我們使用模擬的外部服務及其不同的(localhostURL
  • TestWithStableServer:使用穩定的服務器進行測試。我們調用我們自己的服務,並驗證我們自己的服務發送的請求(必須是一個)是正確的。
  • TestWithUnstableServer:一種非常類似的方法,但是由於外部服務表現不穩定,因此預計將發送兩個請求,並且我們有一個重試策略來處理該請求。

興趣點

關於.NET Core的集成測試,有很好的文檔。也有很好的文檔關於WireMock.NET。我剛剛解釋瞭如何結合這些技術,這實際上是一個與衆不同且被低估的主題。集成測試是實現良好代碼覆蓋率,通過REST調用測試應用程序而無需託管和部署的一種非常好的方法,並使測試變得現實,因爲不需要模擬內部依賴項。但是,仍然需要模擬外部依賴關係。否則,測試失敗並不意味着您自己的應用程序太多(外部應用程序可能已關閉),測試成功也並不意味着太多(它無法處理外部服務的意外失敗)。因此,WireMock.NET可以爲您提供幫助。它使您的測試更有意義。

如果您對完整的源代碼感興趣,請訪問GitHub

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章