猿问

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

所以我有一个使用 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 。如果您能深入了解如何编写控制器以在仍可测试的情况下进行服务调用,我们将不胜感激。


慕容3067478
浏览 195回答 2
2回答

慕工程0101907

所有使测试变慢的依赖项都应该被抽象出来。用抽象包裹HttpClient起来,您可以在测试中模拟它。public interface IMyClient{&nbsp; &nbsp; Task<string> GetRawDataFrom(string url);}那么你的控制器将依赖于那个抽象public class DemoController : Controller{&nbsp; &nbsp; private readonly IMyClient _client;&nbsp; &nbsp; private string _baseUrl = "http://localhost:90/webservice";&nbsp; &nbsp; public DemoController(IMyClient client)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; _client = client;&nbsp; &nbsp; }&nbsp; &nbsp; public async Task<ActionResult> Index()&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var rawData = _client.GetRawDataFrom($"{_baseUrl}vehicle/menu/year");&nbsp; &nbsp; &nbsp; &nbsp; using (var reader = new StringReader(rawData))&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var result =&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(reader);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return View("Index", result);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}然后在测试中,您可以模拟您的抽象以返回预期数据public class FakeClient : IMyClient{&nbsp; &nbsp; &nbsp;public string RawData { get; set; }&nbsp; &nbsp; &nbsp;public Task<string> GetRawDataFrom(string url)&nbsp; &nbsp; &nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return Task.FromResult(RawData);&nbsp; &nbsp; &nbsp;}}[TestMethod]public async Task Test_Index(){&nbsp; &nbsp; // Arrange&nbsp; &nbsp; var fakeClient = new FakeClient&nbsp;&nbsp; &nbsp; {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; RawData = @"[&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { Name: "One", Path: "/one" },&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { Name: "Two", Path: "/two" }&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; ]"&nbsp;&nbsp; &nbsp; };&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; DemoController controller = new DemoController(fakeClient);&nbsp; &nbsp; // Act&nbsp; &nbsp; var result = await controller.Index();&nbsp; &nbsp; ViewResult viewResult = (ViewResult)result;&nbsp; &nbsp; // Assert&nbsp; &nbsp; Assert.AreEqual("Index", viewResult.ViewName);&nbsp; &nbsp; Assert.IsNotNull(viewResult.Model);}实际实施将使用 HttpClientpublic class MyHttpClient : IMyClient{&nbsp;&nbsp; &nbsp; &nbsp;public Task<string> GetRawDataFrom(string url)&nbsp; &nbsp; &nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;var response = await client.GetAsync(url);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (response.IsSuccessStatusCode)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return await response.Content.ReadAsStringAsync();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}&nbsp; &nbsp; &nbsp;}}

BIG阳

在HttpClient没有服务包装器、模拟或 IoC 容器的情况下测试调用的另一种方法是使用Flurl,这是一个小型包装器库HttpClient,提供(除其他外)一些强大的测试功能。[免责声明:我是作者]这是您的控制器的外观。有几种方法可以做到这一点,但这种方法使用字符串扩展方法来完全抽象出客户端。(HttpClient为您管理每个主机的单个实例以防止出现问题。)using Flurl.Http;public class DemoController : Controller{&nbsp; &nbsp; string baseUrl = "http://localhost:90/webservice";&nbsp; &nbsp; // GET: DemoInfo&nbsp; &nbsp; public async Task<ActionResult> Index()&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; var content = await baseUrl&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .AppendPathSegment("vehicle/menu/year")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .GetStringAsync();&nbsp; &nbsp; &nbsp; &nbsp; var result = (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(new StringReader(content));&nbsp; &nbsp; &nbsp; &nbsp; return View("Index", result);&nbsp; &nbsp; }}和测试:using Flurl.Http;[TestMethod]public async Task Test_Index(){&nbsp; &nbsp; // fake & record all HTTP calls in the test subject&nbsp; &nbsp; using (var httpTest = new HttpTest())&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; // Arrange&nbsp; &nbsp; &nbsp; &nbsp; httpTest.RespondWith(200, "<xml>some fake response xml...</xml>");&nbsp; &nbsp; &nbsp; &nbsp; DemoController controller = new DemoController();&nbsp; &nbsp; &nbsp; &nbsp; // Act&nbsp; &nbsp; &nbsp; &nbsp; var result = await controller.Index();&nbsp; &nbsp; &nbsp; &nbsp; ViewResult viewResult = (ViewResult) result;&nbsp; &nbsp; &nbsp; &nbsp; // Assert&nbsp; &nbsp; &nbsp; &nbsp; Assert.AreEqual("Index", viewResult.ViewName);&nbsp; &nbsp; &nbsp; &nbsp; Assert.IsNotNull(viewResult.Model);&nbsp; &nbsp; }}Flurl.Http 在NuGet上可用。
随时随地看视频慕课网APP
我要回答