是否可以模拟 .NET HttpWebResponse?

Posted

技术标签:

【中文标题】是否可以模拟 .NET HttpWebResponse?【英文标题】:Is it possible to mock out a .NET HttpWebResponse? 【发布时间】:2012-04-07 01:25:07 【问题描述】:

我有一个集成测试,可以从第三方服务器获取一些 json 结果。这真的很简单,而且效果很好。

我希望停止实际访问此服务器并使用 Moq(或任何 Mocking 库,如 ninject 等)劫持并强制返回结果。

这可能吗?

这是一些示例代码:-

public Foo GoGetSomeJsonForMePleaseKThxBai()

    // prep stuff ...

    // Now get json please.
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create("Http://some.fancypants.site/api/hiThere);
    httpWebRequest.Method = WebRequestMethods.Http.Get;

    string responseText;

    using (var httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse())
    
        using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
        
            json = streamReader.ReadToEnd().ToLowerInvariant();
        
    

    // Check the value of the json... etc..

当然,这个方法是从我的测试中调用的。

我在想也许我需要传递给这个方法(或类的属性?)一个模拟的httpWebResponse 或其他东西,但不太确定这是否是方法。此外,响应是httpWebRequest.GetResponse() 方法的输出......所以也许我只需要传入一个模拟的HttpWebRequest ?.

任何带有示例代码的建议都将受到高度重视!

【问题讨论】:

【参考方案1】:

与其模拟 HttpWebResponse,不如将调用封装在接口后面,然后模拟该接口。

如果您正在测试 Web 响应是否也命中了我想要的站点,这与 A 类是否调用 WebResponse 接口以获取所需数据的测试不同。

对于模拟界面,我更喜欢Rhino mocks。使用方法见here。

【讨论】:

方法(上图)实际上是一个隐含的接口。所以班级是public class AwesomeClass : IThinkICan。所以你建议我只是模拟那个类,所以我模拟返回对象?现在,我的测试确实击中了第 3 方,但我不希望它这样做。 (即从集成测试转到单元测试)。【参考方案2】:

Microsoft 的 HTTP 堆栈在开发时都没有考虑到单元测试和分离。

您有三个选择:

使对 Web 的调用尽可能小(即发送和取回数据并传递给其他方法)并测试其余部分。就网络通话而言,那里应该发生很多神奇的事情,而且非常简单。 将 HTTP 调用封装在另一个类中,并在测试时传递您的模拟对象。 用另外两个类包装HttpWebResponseHttpWebRequest。这就是 MVC 团队对 HttpContext 所做的事情。

第二个选项:

interface IWebCaller

    string CallWeb(string address);

【讨论】:

【参考方案3】:

您可能希望更改您的消费代码以接收一个工厂接口,该接口创建可以模拟的请求和响应,这些请求和响应包装了实际的实现。

更新:重温

在我的答案被接受后很长一段时间内,我一直在投票,我承认我最初的答案质量很差,并且做了很大的假设。

在 4.5+ 中模拟 HttpWebRequest

我原来的答案的困惑在于你可以在 4.5 中模拟 HttpWebResponse,但不能在早期版本中模拟。在 4.5 中模拟它也使用过时的构造函数。因此,推荐的做法是抽象请求和响应。无论如何,下面是使用 .NET 4.5 和 Moq 4.2 的完整工作测试。

[Test]
public void Create_should_create_request_and_respond_with_stream()

    // arrange
    var expected = "response content";
    var expectedBytes = Encoding.UTF8.GetBytes(expected);
    var responseStream = new MemoryStream();
    responseStream.Write(expectedBytes, 0, expectedBytes.Length);
    responseStream.Seek(0, SeekOrigin.Begin);

    var response = new Mock<HttpWebResponse>();
    response.Setup(c => c.GetResponseStream()).Returns(responseStream);

    var request = new Mock<HttpWebRequest>();
    request.Setup(c => c.GetResponse()).Returns(response.Object);

    var factory = new Mock<IHttpWebRequestFactory>();
    factory.Setup(c => c.Create(It.IsAny<string>()))
        .Returns(request.Object);

    // act
    var actualRequest = factory.Object.Create("http://www.google.com");
    actualRequest.Method = WebRequestMethods.Http.Get;

    string actual;

    using (var httpWebResponse = (HttpWebResponse)actualRequest.GetResponse())
    
        using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
        
            actual = streamReader.ReadToEnd();
        
    


    // assert
    actual.Should().Be(expected);


public interface IHttpWebRequestFactory

    HttpWebRequest Create(string uri);

更好的答案:抽象响应和请求

这是一个更安全的抽象实现,适用于以前的版本(嗯,至少低至 3.5):

[Test]
public void Create_should_create_request_and_respond_with_stream()

    // arrange
    var expected = "response content";
    var expectedBytes = Encoding.UTF8.GetBytes(expected);
    var responseStream = new MemoryStream();
    responseStream.Write(expectedBytes, 0, expectedBytes.Length);
    responseStream.Seek(0, SeekOrigin.Begin);

    var response = new Mock<IHttpWebResponse>();
    response.Setup(c => c.GetResponseStream()).Returns(responseStream);

    var request = new Mock<IHttpWebRequest>();
    request.Setup(c => c.GetResponse()).Returns(response.Object);

    var factory = new Mock<IHttpWebRequestFactory>();
    factory.Setup(c => c.Create(It.IsAny<string>()))
        .Returns(request.Object);

    // act
    var actualRequest = factory.Object.Create("http://www.google.com");
    actualRequest.Method = WebRequestMethods.Http.Get;

    string actual;

    using (var httpWebResponse = actualRequest.GetResponse())
    
        using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
        
            actual = streamReader.ReadToEnd();
        
    


    // assert
    actual.Should().Be(expected);


public interface IHttpWebRequest

    // expose the members you need
    string Method  get; set; 

    IHttpWebResponse GetResponse();


public interface IHttpWebResponse : IDisposable

    // expose the members you need
    Stream GetResponseStream();


public interface IHttpWebRequestFactory

    IHttpWebRequest Create(string uri);


// barebones implementation

private class HttpWebRequestFactory : IHttpWebRequestFactory

    public IHttpWebRequest Create(string uri)
    
        return new WrapHttpWebRequest((HttpWebRequest)WebRequest.Create(uri));
    


public class WrapHttpWebRequest : IHttpWebRequest

    private readonly HttpWebRequest _request;

    public WrapHttpWebRequest(HttpWebRequest request)
    
        _request = request;
    

    public string Method
    
        get  return _request.Method; 
        set  _request.Method = value; 
    

    public IHttpWebResponse GetResponse()
    
        return new WrapHttpWebResponse((HttpWebResponse)_request.GetResponse());
    


public class WrapHttpWebResponse : IHttpWebResponse

    private WebResponse _response;

    public WrapHttpWebResponse(HttpWebResponse response)
    
        _response = response;
    

    public void Dispose()
    
        Dispose(true);
        GC.SuppressFinalize(this);
    

    private void Dispose(bool disposing)
    
        if (disposing)
        
            if (_response != null)
            
                ((IDisposable)_response).Dispose();
                _response = null;
            
        
    

    public Stream GetResponseStream()
    
        return _response.GetResponseStream();
    

【讨论】:

您不能为 HttpWebResponse 创建模拟。您将收到如下错误:“System.ArgumentException:无法实例化类的代理:System.Net.HttpWebResponse。找不到无参数构造函数。参数名称:constructorArguments” 我找不到支持 Is IHttpWebRequest、IHttpWebRequestFactory、IHttpWebResponse 的 .Net 框架。有什么想法吗? @AjitGoel 它们是您必须创建的接口,我在示例中提供了这些接口。向下滚动第二个代码示例,它就在那里。 @HackedByChinese:不知道我是怎么错过的。需要咖啡来唤醒我。 使用 IHttpWebRequestFactory 有充分的理由吗?我们不能把 Create 方法保留在 IHttpWebRequest 接口中吗?【参考方案4】:

您实际上可以在不嘲笑的情况下返回HttpWebResponse,请参阅我的回答here。它不需要任何“外部”代理接口,只需要“标准”WebRequestWebResponseICreateWebRequest

如果您不需要访问HttpWebResponse 而只需处理WebResponse,那就更容易了;我们在单元测试中这样做是为了返回“预制”内容响应以供消费。我必须“加倍努力”才能返回实际的 HTTP 状态代码,以模拟例如404 响应要求您使用 HttpWebResponse,以便您可以访问 StatusCode 属性等。

假设一切都是HttpWebXXX的其他解决方案忽略WebRequest.Create()支持的所有东西,除了HTTP,它可以是你想使用的任何注册前缀的处理程序(通过WebRequest.RegisterPrefix(),如果你忽略这一点,您就错过了,因为它是公开其他您无法访问的内容流的好方法,例如嵌入资源流、文件流等。

此外,将WebRequest.Create() 的返回显式转换为HttpWebRequest破坏路径,因为方法返回类型是WebRequest,并且再次表明对该API 的实际情况有些无知有效。

【讨论】:

【参考方案5】:

我在blog post 中找到了一个很好的解决方案:

它非常易于使用,您只需要这样做:

string response = "my response string here";
WebRequest.RegisterPrefix("test", new TestWebRequestCreate());
TestWebRequest request = TestWebRequestCreate.CreateTestRequest(response);

并将这些文件复制到您的项目中:

    class TestWebRequestCreate : IWebRequestCreate

    static WebRequest nextRequest;
    static object lockObject = new object();

    static public WebRequest NextRequest
    
        get  return nextRequest ;
        set
        
            lock (lockObject)
            
                nextRequest = value;
            
        
    

    /// <summary>See <see cref="IWebRequestCreate.Create"/>.</summary>
    public WebRequest Create(Uri uri)
    
        return nextRequest;
    

    /// <summary>Utility method for creating a TestWebRequest and setting
    /// it to be the next WebRequest to use.</summary>
    /// <param name="response">The response the TestWebRequest will return.</param>
    public static TestWebRequest CreateTestRequest(string response)
    
        TestWebRequest request = new TestWebRequest(response);
        NextRequest = request;
        return request;
    


class TestWebRequest : WebRequest

    MemoryStream requestStream = new MemoryStream();
    MemoryStream responseStream;

    public override string Method  get; set; 
    public override string ContentType  get; set; 
    public override long ContentLength  get; set; 

    /// <summary>Initializes a new instance of <see cref="TestWebRequest"/>
    /// with the response to return.</summary>
    public TestWebRequest(string response)
    
        responseStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(response));
    

    /// <summary>Returns the request contents as a string.</summary>
    public string ContentAsString()
    
        return System.Text.Encoding.UTF8.GetString(requestStream.ToArray());
    

    /// <summary>See <see cref="WebRequest.GetRequestStream"/>.</summary>
    public override Stream GetRequestStream()
    
        return requestStream;
    

    /// <summary>See <see cref="WebRequest.GetResponse"/>.</summary>
    public override WebResponse GetResponse()
    
        return new TestWebReponse(responseStream);
    


class TestWebReponse : WebResponse

    Stream responseStream;

    /// <summary>Initializes a new instance of <see cref="TestWebReponse"/>
    /// with the response stream to return.</summary>
    public TestWebReponse(Stream responseStream)
    
        this.responseStream = responseStream;
    

    /// <summary>See <see cref="WebResponse.GetResponseStream"/>.</summary>
    public override Stream GetResponseStream()
    
        return responseStream;
    

【讨论】:

这没有引用 HttpWebResponse。一旦您将 TestWebResponse 转换为 HttpWebResponse ,它将为空。 @Christo 当您调用 WebRequest.RegisterPrefix("test", new TestWebRequestCreate()); 时,您正在为请求注册前缀。所以你也必须模拟 url:使用 test://example.com 而不是 http://example.com 您发现的帖子很酷,对于测试 WebRequest/Response 等很有用。但最初的问题是我如何测试 HttpWebRequest。如果您使用此代码测试问题中的代码,则尝试将TestWebReponse 显式转换为HttpWebRequest 将失败。他方法的第 4 行 @Christo 我认为您的意思是将TestWebReponse 转换为HttpWebResponseTestWebRequest 转换为HttpWebRequest。在这种情况下,您只需将继承自 TestWebRequestTestWebReponse 更改为分别继承自 HttpWebRequestHttpWebResponse 抱歉错字。我确实是那个意思。我建议您测试这些建议,因为我认为这行不通。【参考方案6】:

如果有帮助,请在下面使用 NSubstitute 代替 Moq 找到接受的答案中说明的代码

using NSubstitute; /*+ other assemblies*/

[TestMethod]
public void Create_should_create_request_and_respond_with_stream()

   //Arrange
   var expected = "response content";
   var expectedBytes = Encoding.UTF8.GetBytes(expected);
   var responseStream = new MemoryStream();
   responseStream.Write(expectedBytes, 0, expectedBytes.Length);
   responseStream.Seek(0, SeekOrigin.Begin);

   var response = Substitute.For<HttpWebResponse>();
   response.GetResponseStream().Returns(responseStream);

   var request = Substitute.For<HttpWebRequest>();
   request.GetResponse().Returns(response);

   var factory = Substitute.For<IHttpWebRequestFactory>();
   factory.Create(Arg.Any<string>()).Returns(request);

   //Act
   var actualRequest = factory.Create("http://www.google.com");
   actualRequest.Method = WebRequestMethods.Http.Get;

   string actual;

   using (var httpWebResponse = (HttpWebResponse)actualRequest.GetResponse())
   
       using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
       
           actual = streamReader.ReadToEnd();
       
   

   //Assert
   Assert.AreEqual(expected, actual);


public interface IHttpWebRequestFactory

    HttpWebRequest Create(string uri);

单元测试运行并成功通过。

我一直在寻找如何有效地做到这一点的答案。

【讨论】:

投反对票,因为这根本不起作用:var r = Substitute.For&lt;HttpWebResponse&gt;(); Castle.DynamicProxy.InvalidProxyConstructorArgumentsException: Can not instantiate proxy of class: System.Net.HttpWebResponse. 该死,无法编辑我的评论,所以将添加另一个:您不能使用此类的 .Net 3.5 变体的替代品。更高版本有默认构造函数。但在更高版本中,默认构造函数也是公共的,因此您可以简单地实例化此类而不是模拟【参考方案7】:

虽然有一个公认的答案,但我想分享我的观点。很多时候,修改源代码并不是建议的选择。此外,答案至少引入了很多生产代码。

所以我更喜欢使用免费的harmonyLib 并通过拦截WebRequest.Create 静态方法来修补CLR,而不需要对生产代码进行任何更改。

我修改后的版本如下:

        public static bool CreateWithSomeContent(ref WebRequest __result)
            var expected = "response content";
            var expectedBytes = Encoding.UTF8.GetBytes(expected);
            var responseStream = new MemoryStream();
            responseStream.Write(expectedBytes, 0, expectedBytes.Length);
            responseStream.Seek(0, SeekOrigin.Begin);

            var response = new Mock<HttpWebResponse>();
            response.Setup(c => c.GetResponseStream()).Returns(responseStream);

            var requestMock = new Mock<HttpWebRequest>();
            requestMock.Setup(c => c.GetResponse()).Returns(response.Object);
            __result = requestMock.Object;
            return false;
        

        [Test]
        public void Create_should_create_request_and_respond_with_stream()
            //arrange
            var harmony = new Harmony(nameof(Create_should_create_request_and_respond_with_stream));
            var methodInfo = typeof(WebRequest).GetMethod("Create",new Type[]typeof(string));
            harmony.Patch(methodInfo, new HarmonyMethod(GetType(), nameof(CreateWithSomeContent)));

            //act
            var actualRequest = WebRequest.Create("");
            string actual;
            using (var httpWebResponse = (HttpWebResponse)actualRequest.GetResponse())
                using (var streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
                    actual = streamReader.ReadToEnd();
                
            
            
            // assert
            // actual.Should().Be("response content");
        

【讨论】:

以上是关于是否可以模拟 .NET HttpWebResponse?的主要内容,如果未能解决你的问题,请参考以下文章

在 .NET 中模拟 VBA 算法

.NET Core 管道模型中间件及管道模拟实现

精:.NET Core 管道模型中间件及管道模拟实现

带有服务的 SQL Server 的模拟/外观?

您如何在 .NET 中进行模拟?

.NET 中的单元测试:如何模拟实体数据提供者