【问题标题】:Unit test Controller with HttpClient in .NET MVC.NET MVC 中带有 HttpClient 的单元测试控制器
【发布时间】:2018-08-04 03:56:11
【问题描述】:

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

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。非常感谢任何帮助,让您深入了解如何编写控制器以在仍可测试的同时进行服务调用。

【问题讨论】:

    标签: c# .net asp.net-mvc unit-testing dotnet-httpclient


    【解决方案1】:

    所有导致测试变慢的依赖都应该被抽象出来。
    用抽象包装 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);
            }
        }
    }
    

    然后在测试中你可以模拟你的抽象来返回预期的数据

    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 容器,以便在实际应用程序运行时注入MyHttpClient。这是正确的吗?
    • @imcz,ASP.NET有自己的内置依赖注入,你可以使用。
    【解决方案2】:

    在没有服务包装、模拟或 IoC 容器的情况下测试HttpClient 调用的另一种方法是使用Flurl,这是一个围绕HttpClient 的小型包装库,它提供(除其他外)一些强大的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 上可用。

    【讨论】:

      猜你喜欢
      • 2011-01-30
      • 2011-12-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多