在c#中对HTTP请求进行单元测试

Cei*_*ish 31 .net c# unit-testing

我正在编写一些调用Web服务的代码,读取响应并对其执行某些操作.我的代码名义上看起来像这样:

string body = CreateHttpBody(regularExpression, strategy);

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
request.Method = "POST";
request.ContentType = "text/plain; charset=utf-8";

using (Stream requestStream = request.GetRequestStream())
{
    requestStream.Write(Encoding.UTF8.GetBytes(body), 0, body.Length);
    requestStream.Flush();
}

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
    byte[] data = new byte[response.ContentLength];

    using (Stream stream = response.GetResponseStream())
    {
        int bytesRead = 0;

        while (bytesRead < data.Length)
        {
            bytesRead += stream.Read(data, bytesRead, data.Length - bytesRead);
        }
    }

    return ExtractResponse(Encoding.UTF8.GetString(data));
}
Run Code Online (Sandbox Code Playgroud)

我实际上进行任何自定义操作的唯一部分是在ExtractResponseCreateHttpBody方法中.然而,仅仅对这些方法进行单元测试感觉是错误的,并且希望其余的代码能够正确地组合在一起.有什么方法可以拦截HTTP请求并提供模拟数据吗?

编辑此信息现已过期.使用System.Net.Http.HttpClient库构建此类代码要容易得多.

Sjo*_*erd 21

在您的代码中,您无法拦截调用,HttpWebRequest因为您使用相同的方法创建对象.如果让另一个对象创建HttpWebRequest,则可以传入一个模拟对象并使用它来测试.

所以不是这样的:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
Run Code Online (Sandbox Code Playgroud)

用这个:

IHttpWebRequest request = this.WebRequestFactory.Create(_url);
Run Code Online (Sandbox Code Playgroud)

在单元测试中,您可以传入一个WebRequestFactory创建模拟对象.

此外,您可以在单独的函数中拆分流读取代码:

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
    byte[] data = ReadStream(response.GetResponseStream());
    return ExtractResponse(Encoding.UTF8.GetString(data));
}
Run Code Online (Sandbox Code Playgroud)

这使得可以ReadStream()单独测试.

要进行更多集成测试,您可以设置自己的HTTP服务器,该服务器返回测试数据,并将该服务器的URL传递给您的方法.


Jef*_*mon 7

如果模拟出HttpWebRequest和HttpWebResponse变得过于繁琐,或者你需要在验证测试中测试代码,你从"外部"调用代码,那么创建虚假服务可能是最好的方法.

我实际上写了一个名为MockHttpServer的开源库来帮助解决这个问题,这使得模拟任何通过HTTP进行通信的外部服务变得非常简单.

下面是一个使用它与RestSharp一起调用API端点的示例,但HttpWebRequest也可以正常工作.

using (new MockServer(3333, "/api/customer", (req, rsp, prm) => "Result Body"))
{
    var client = new RestClient("http://localhost:3333/");
    var result = client.Execute(new RestRequest("/api/customer", Method.GET));
}
Run Code Online (Sandbox Code Playgroud)

GitHub页面上有一个相当详细的自述文件,其中介绍了可供使用它的所有选项,并且库本身可通过NuGet获得.


Dar*_*rov 6

我可能会首先重构代码,以使其更弱地耦合到实际的HTTP请求.现在这段代码似乎做了很多事情.

这可以通过引入抽象来完成:

public interface IDataRetriever
{
    public byte[] RetrieveData(byte[] request);
}
Run Code Online (Sandbox Code Playgroud)

现在,您尝试进行单元测试的类可以使用Inversion of Control设计模式与实际的HTTP请求分离:

public class ClassToTest
{
    private readonly IDataRetriever _dataRetriever;
    public Foo(IDataRetriever dataRetriever)
    {
        _dataRetriever = dataRetriever;
    }

    public string MethodToTest(string regularExpression, string strategy)
    {
        string body = CreateHttpBody(regularExpression, strategy);
        byte[] result = _dataRetriever.RetrieveData(Encoding.UTF8.GetBytes(body));
        return ExtractResponse(Encoding.UTF8.GetString(result));
    }
}
Run Code Online (Sandbox Code Playgroud)

处理实际的HTTP请求不再是ClassToTest的责任.它现在已经解耦了.测试MethodToTest成为一项微不足道的任务.

最后一部分显然是要实现我们引入的抽象:

public class MyDataRetriever : IDataRetriever
{
    private readonly string _url;
    public MyDataRetriever(string url)
    {
        _url = url;
    }

    public byte[] RetrieveData(byte[] request)
    {
        using (var client = new WebClient())
        {
            client.Headers[HttpRequestHeader.ContentType] = "text/plain; charset=utf-8";
            return client.UploadData(_url, request);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以配置您喜欢的DI框架,将MyDataRetriever实例注入ClassToTest实际应用程序中的类构造函数.

  • 没错,我不想测试那些类,我想测试那些类的_usage_.发送和接收HTTP请求的代码不是完全隐藏的,因此它依赖于我的代码是正确的,以成功读取请求的响应. (3认同)
  • @Ceilingfish,不,你绝对不想测试HttpWebRequest和HttpWebResponse类.这些已经过微软的广泛测试.您要测试的是,您的方法为某些黑盒子(提供给您并且预期可以工作)提供了正确的输入,并且它将其输出转换为某种预期格式.这基本上就是你的方法所做的.无论如何,如果没有这种抽象级别,你就不能模拟那些类,你不再进行单元测试而是进行集成测试. (2认同)