imcz
imcz

Reputation: 312

Unit test Controller with HttpClient in .NET MVC

So I have a controller that is using HttpClient to call a webservice like so:

public class DemoController : Controller
{
    HttpClient client;
    string baseUrl = "http://localhost:90/webservice";

    public DemoController()
    {
        client = new HttpClient
        {
            BaseAddress = new Uri(baseUrl)
        };

    }

    // GET: DemoInfo
    public async Task<ActionResult> Index()
    {
        HttpResponseMessage response = await client.GetAsync(baseUrl + "vehicle/menu/year");
        string content = "";
        MenuItems result = null;
        if (response.IsSuccessStatusCode)
        {
            content = await response.Content.ReadAsStringAsync();
            result = (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(new StringReader(content));
        }
        return View("Index", result);
    }
}

My unit test for this action is as follows:

    [TestMethod]
    public async Task Test_Index()
    {
        // Arrange
        DemoController controller = new DemoController();

        // Act
        var result = await controller.Index();
        ViewResult viewResult = (ViewResult) result;

        // Assert
        Assert.AreEqual("Index", viewResult.ViewName);
        Assert.IsNotNull(viewResult.Model);
    }

So obviously I would like to avoid making the web service call every time the test is run. Would I be on the right track in opting for an IoC container like Unity so that HttpClient would be injected into the controller? Is that overkill for what I'm trying to achieve? I'm aware that there is a lot of history with people struggling with properly mocking httpclient in there unit tests through this github issue. Any help would be greatly appreciated in giving some insight into how to write the controller to make a service call while still being testable.

Upvotes: 2

Views: 1829

Answers (2)

Todd Menier
Todd Menier

Reputation: 39369

An alternative approach to testing HttpClient calls without service wrappers, mocks, or IoC containers is to use Flurl, a small wrapper library around HttpClient that provides (among other things) some robust testing features. [Disclaimer: I'm the author]

Here's what your controller would look like. There's a few ways to do this, but this approach uses string extension methods that abstract away the client entirely. (A single HttpClient instance per host is managed for you to prevent trouble.)

using Flurl.Http;

public class DemoController : Controller
{
    string baseUrl = "http://localhost:90/webservice";

    // GET: DemoInfo
    public async Task<ActionResult> Index()
    {
        var content = await baseUrl
            .AppendPathSegment("vehicle/menu/year")
            .GetStringAsync();

        var result = (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(new StringReader(content));
        return View("Index", result);
    }
}

And the test:

using Flurl.Http;

[TestMethod]
public async Task Test_Index()
{
    // fake & record all HTTP calls in the test subject
    using (var httpTest = new HttpTest())
    {
        // Arrange
        httpTest.RespondWith(200, "<xml>some fake response xml...</xml>");
        DemoController controller = new DemoController();

        // Act
        var result = await controller.Index();
        ViewResult viewResult = (ViewResult) result;

        // Assert
        Assert.AreEqual("Index", viewResult.ViewName);
        Assert.IsNotNull(viewResult.Model);
    }
}

Flurl.Http is available on NuGet.

Upvotes: 2

Basin
Basin

Reputation: 1017

All dependencies which makes tests slow should be abstracted.
Wrap HttpClient with an abstraction, which you can mock in your tests.

public interface IMyClient
{
    Task<string> GetRawDataFrom(string url);
}

Then your controller will depend on that abstraction

public class DemoController : Controller
{
    private readonly IMyClient _client;
    private string _baseUrl = "http://localhost:90/webservice";

    public DemoController(IMyClient client)
    {
        _client = client;
    }

    public async Task<ActionResult> Index()
    {
        var rawData = _client.GetRawDataFrom($"{_baseUrl}vehicle/menu/year");
        using (var reader = new StringReader(rawData))
        {
            var result = 
                (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(reader);
            return View("Index", result);
        }
    }
}

Then in tests you can mock your abstraction to return expected data

public class FakeClient : IMyClient
{
     public string RawData { get; set; }

     public Task<string> GetRawDataFrom(string url)
     {
         return Task.FromResult(RawData);
     }
}

[TestMethod]
public async Task Test_Index()
{
    // Arrange
    var fakeClient = new FakeClient 
    { 
        RawData = @"[ 
            { Name: "One", Path: "/one" },
            { Name: "Two", Path: "/two" }  
        ]" 
    };       
    DemoController controller = new DemoController(fakeClient);

    // Act
    var result = await controller.Index();
    ViewResult viewResult = (ViewResult)result;

    // Assert
    Assert.AreEqual("Index", viewResult.ViewName);
    Assert.IsNotNull(viewResult.Model);
}

Actual implementation will use HttpClient

public class MyHttpClient : IMyClient
{ 
     public Task<string> GetRawDataFrom(string url)
     {
         var response = await client.GetAsync(url);
         if (response.IsSuccessStatusCode)
         {
             return await response.Content.ReadAsStringAsync();
         }
     }
}

Upvotes: 2

Related Questions