0

We have a few classes in our C# project that make calls out to 3rd party APIs. We're using HttpClient objects for the calls. We've set up our classes where we do these calls to accept an HttpClient so that when testing, we can use a custom/fake DelegatingHandler with the client.

We've set up our classes like this:

public class CallingService : ApiService
{
    private readonly ISomeOtherService _someOtherService;

    public CallingService (ILogger logger, 
        IConfigurationManager configurationManager,
        ISomeOtherService someOtherService) : base(logger, configurationManager)
    {
        _someOtherService = someOtherService;
    }

    public CallingService (ILogger logger,
        HttpClient client,
        IConfigurationManager configurationManager,
        ISomeOtherService someOtherService) : base(logger, configurationManager, client)
    {
        _someOtherService = someOtherService;
    }

    private async Task<XmlNodeList> TransmitToApi(string xml_string)
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls12;
        //..
        string type = "application/xml";
        var content = new StreamContent(new MemoryStream(Encoding.ASCII.GetBytes(xml_string)));
        var targetUri = new Uri(ConfigurationManager.GetAppSetting("ApiUrl"));
        var message = new HttpRequestMessage
        {
            RequestUri = targetUri ,
            Method = HttpMethod.Post,
            Content = content
        };

        message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
        message.Content.Headers.Add("Content-Type", type);
        message.Headers.Add("someHeader", someData);

        HttpResponseMessage response = null;
        try
        {
            // Define the cancellation token.
            CancellationTokenSource source = new CancellationTokenSource();
            CancellationToken token = source.Token;
            response = await Client.SendAsync(message, token);
        }
        catch (Exception ex)
        {
            throw ex;
        }
        //...
        return someData;
    }

The base ApiService class defines a generic HttpClient object if one is not provided.

We're currently using SendAsync so we can define the message headers. (We have more headers than are listed here.)

The test defines the DelegatingHandler like this:

public class FakeResponseHandler : DelegatingHandler
{
    private readonly Dictionary<Uri, HttpResponseMessage> _fakeResponses = new Dictionary<Uri, HttpResponseMessage>();

    public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage, string content = "", bool asXml = false)
    {
        if (!string.IsNullOrWhiteSpace(content))
        {
            if (asXml)
            {
                responseMessage.Content = new StringContent(content, Encoding.UTF8, "application/xml");
            }
            else
            {
                responseMessage.Content = new StringContent(content, Encoding.UTF8, "application/json");
            }
        }
        _fakeResponses.Add(uri, responseMessage);
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (_fakeResponses.ContainsKey(request.RequestUri))
        {
            return _fakeResponses[request.RequestUri];
        }
        return new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request };
    }
}

And then:

[Fact]
public async Task ItWillDoStuffAndCallApi()
{
    using (var mock = AutoMock.GetLoose())
    {
        mock.Mock<IConfigurationManager>()
            .Setup(cm => cm.GetAppSetting("ApiUrl"))
            .Returns("http://example.org/test/");

        string testReturnData = GetFileContents("IntegrationTests.SampleData.SampleApiResponseXML.txt");
        FakeResponseHandler fakeResponseHandler = new FakeResponseHandler();
        fakeResponseHandler.AddFakeResponse(new Uri("http://example.org/test/"),
            new HttpResponseMessage(HttpStatusCode.OK),
            testReturnData,
            true);

        //HttpClient httpClient = new HttpClient(fakeResponseHandler);
        HttpClient httpClient = HttpClientFactory.Create(fakeResponseHandler);
        mock.Provide(httpClient);

        var ourService = new CallingService();
        ourService.TransmitToApi(someXmlString);
    }
}

When we run the test, we receive the message:

Handler did not return a response message.

And we never seem to get into DelegatingHandler.SendAsync method.

We have other classes calling APIs using HttpClient.PostAsync or GetAsync, and these do call the DelegatingHandler.SendAsync method and work as expected.

We've tried:

HttpClient httpClient = new HttpClient(fakeResponseHandler);

and
HttpClient httpClient = HttpClientFactory.Create(fakeResponseHandler);

We've also tried Client.SendAsync with and without the cancellation token.

Why is this not working?

Should we re-write this to use PostAsync?

M Kenyon II
  • 4,136
  • 4
  • 46
  • 94

1 Answers1

0

I'd need to see the implementation of HttpClientFactory.Create and what Client.SendAsync actually does internally but nevertheless I was able to use the sample code you provide and fill in the blanks where I could to get the following to work:

public class FakeResponseHandler : DelegatingHandler
{
    private readonly Dictionary<Uri, HttpResponseMessage> _fakeResponses = new Dictionary<Uri, HttpResponseMessage>();

    public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage, string content = "", bool asXml = false)
    {
        if (!string.IsNullOrWhiteSpace(content))
        {
            if (asXml)
            {
                responseMessage.Content = new StringContent(content, Encoding.UTF8, "application/xml");
            }
            else
            {
                responseMessage.Content = new StringContent(content, Encoding.UTF8, "application/json");
            }
        }
        _fakeResponses.Add(uri, responseMessage);
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var emptyContent = string.Empty;

        if (request.Content.Headers.ContentType.MediaType == "application/xml")
            emptyContent = "<empty />";

        return Task.FromResult(_fakeResponses.ContainsKey(request.RequestUri) ?
            _fakeResponses[request.RequestUri] :
            new HttpResponseMessage(HttpStatusCode.NotFound)
            {
                RequestMessage = request,
                Content = new StringContent(emptyContent)
            });
    }
}

Just to make things clean use Task.FromResult to return a task in SendAsync and also provide an empty content to avoid null reference exceptions.

public class CallingService
{
    private readonly HttpClient _httpClient;
    private readonly IConfigurationManager _configurationManager;

    public CallingService(HttpClient httpClient, 
        IConfigurationManager configurationManager)
    {
        _httpClient = httpClient;
        _configurationManager = configurationManager;
    }

    public async Task<XmlNodeList> TransmitToApi(string xml_string)
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls12;
        //..
        string type = "application/xml";
        var content = new StreamContent(new MemoryStream(Encoding.ASCII.GetBytes(xml_string)));
        var targetUri = new Uri(_configurationManager.GetAppSetting("ApiUrl"));
        var message = new HttpRequestMessage
        {
            RequestUri = targetUri,
            Method = HttpMethod.Post,
            Content = content
        };

        message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
        message.Content.Headers.Add("Content-Type", type);

        string somedata;
        try
        {
            // Define the cancellation token.
            CancellationTokenSource source = new CancellationTokenSource();
            CancellationToken token = source.Token;
            var response = await _httpClient.SendAsync(message, token);

            somedata = await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            throw ex;
        }
        //...
        var xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(somedata);
        return xmlDoc.SelectNodes("*");
    }
}

And then the test passes the instance of HttpClient to CallingService:

    [TestMethod]
    public async Task TestMethod1()
    {
        const string content = @"<root><test>1243</test></root>";
        const string httpExample = "http://example.org/test/";

        var configurationManager = new Mock<IConfigurationManager>();

        configurationManager
            .Setup(cm => cm.GetAppSetting("ApiUrl"))
            .Returns(httpExample);

        var fakeResponseHandler = new FakeResponseHandler();
        fakeResponseHandler.AddFakeResponse(new Uri(httpExample),
            new HttpResponseMessage(HttpStatusCode.OK), content, true);

        using (var httpClient = new HttpClient(fakeResponseHandler))
        {
            var ourService = new CallingService(httpClient, configurationManager.Object);

            var result = await ourService.TransmitToApi(content);

            Assert.AreEqual(content, result.Item(0)?.OuterXml);
        }
    }

This all works so if I had to guess - the issue would be somewhere in your HttpClientFacotry.

Hope that helps!! Cheers, :)

TheRock
  • 1,513
  • 1
  • 18
  • 19