Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

How WireMock.NET Can Help in Doing Integration Testing of a .NET Core Application

5.00/5 (4 votes)
19 Mar 2021CPOL3 min read 26.4K  
Learn how to do integration testing while mocking external dependencies of your .NET Core application
Many applications are just unit tested with xUnit and not integration tested. .NET Core provides nice possibilities to do integration testing. Your tests will be more realistic than unit tests since only external dependencies will be mocked and internal dependencies won't. WireMock.NET provides the ways to do this.

Introduction

If you are an ASP.NET Core developer doing TDD, you probably face some problems. Your Program class and Startup class are not covered by your tests. Your mocking framework helps to mock internal dependencies but not to do the same with external dependencies, such as web services made by other companies. Moreover, maybe you decided to just not test some class because there are too many internal dependencies to mock. In this article, I'll explain how to solve these problems.

Background

It will help a lot if you have some experience with TDD for .NET Core 3.1 (the version I use here), preferably with xUnit.

Using the Code

First, let's implement the ConfigureServices method. We depend on an external service set in the appsettings.json file and on a class depending on HttpClient.

A retry policy is added to ensure requests are retried if these accidentally fail.

C#
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)));
}

Moreover, this class to instantiate for dependency injection (into the controller) needs to be implemented as well. There is just one method. It calls an external service and returns the number of characters.

C#
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;
   }
}

Logically, we need to implement the controller too.

C#
[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);
     }
}

To test everything with a web request from an automated test, we need to do self-hosting of the web application (during the xUnit test). To do this, we need the WebApplicationFactory in the base class shown here:

C#
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);
    }
}

This base class does the following things:

  • Creates a HttpClient to do REST calls to our own application without starting it (done by CreateClient)
  • Runs the code in the Startup and Program class (also done by CreateClient)
  • Updates the configuration specifically for our tests with AddInMemoryCollection
  • Disposes the HttpClient after each test

Now that we have the base class, we can implement the actual tests.

C#
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));
    }
}

Both the web application and the external service are self-hosted. There is no need to start one of these. We test like the way we do with unit tests. This is what the methods do:

  • SetupStableServer: We set up a mocked external service and ensure it behaves like a stable service. It always returns a response with status code 200.
  • SetupUnStableServer: This is to setup a mocked external service returning a 200 after the first request failed (500, Internal Server Error)
  • Dispose: Stop the external service
  • GetConfiguration: Return the new configuration settings. We use the mocked external service with its different (localhost) url.
  • TestWithStableServer: Do a test with stable server. We call our own service and verify the request (it has to be just one) sent by our own service is correct.
  • TestWithUnstableServer: A very similar method but two requests are expected to be sent since the external service is behaving unstable and we have a retry policy to deal with that.

Points of Interest

There is good documentation about integration testing with .NET Core. There is also good documentation about WireMock.NET. I just explained how to combine those technologies which is really a different and underestimated topic. Integration testing is a really nice way to achieve good code coverage, test the application via REST Calls without hosting & deploying and make the tests realistic because internal dependencies do not need to be mocked. However, external dependencies still need to be mocked. Otherwise, failure of the tests does not mean so much about your own application (the external application may be down) and success also does not mean so much (it may not be able to deal with accidental failure of external services). Therefore, WireMock.NET is there to help you. It makes your tests more meaningful.

If you are interested in the full source code, it is on GitHub.

History

  • 7th May, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)