.NET MVC 中带有 HttpClient 的单元测试控制器

Unit test Controller with HttpClient in .NET MVC

所以我有一个控制器使用 HttpClient 调用网络服务,如下所示:

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

我对这个动作的单元测试如下:

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

所以显然我想避免在每次测试 运行 时都进行 Web 服务调用。我是否会选择像 Unity 这样的 IoC 容器,以便将 HttpClient 注入控制器?对于我想要实现的目标来说,这是否有点矫枉过正?我知道有很多历史,人们通过这个 github issue 在单元测试中努力正确模拟 httpclient。如果您能深入了解如何编写控制器以在仍可测试的情况下进行服务调用,我们将不胜感激。

应该抽象所有使测试变慢的依赖项。
用抽象包装 HttpClient,您可以在测试中对其进行模拟。

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

然后你的控制器将依赖于那个抽象

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

然后在测试中,您可以将抽象模拟为 return 预期数据

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

实际实现将使用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();
         }
     }
}

在没有服务包装器、模拟或 IoC 容器的情况下测试 HttpClient 调用的另一种方法是使用 Flurl, a small wrapper library around HttpClient that provides (among other things) some robust testing features。 [免责声明:我是作者]

这是您的控制器的外观。有几种方法可以做到这一点,但这种方法使用完全抽象出客户端的字符串扩展方法。 (为您管理每个主机的单个 HttpClient 实例,以防止 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);
    }
}

测试:

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 在 NuGet 可用。