有没有可能模拟一个.NET HttpWebResponse?

8fsztsew  于 2023-01-06  发布在  .NET
关注(0)|答案(7)|浏览(149)

我有一个集成测试,从第三方服务器抓取一些json结果。它真的很简单,工作得很好。
我希望停止实际攻击这个服务器,并使用Moq(或任何模拟库,如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?。
任何建议与一些样本代码将是最赞赏!

wvyml7n5

wvyml7n51#

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

更新:重新访问

在我的答案被接受很久之后,我一直在获得反对票,我承认我最初的答案质量很差,而且做了一个很大的假设。

在4.5+中模拟HttpWeb请求

我最初的回答中的困惑在于,你可以在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();
    }
}
2ekbmq32

2ekbmq322#

我不会模仿HttpWebResponse,而是将调用 Package 在一个接口后面,并模仿该接口。
如果您正在测试Web响应是否命中了我也想要的站点,这与类A是否调用WebResponse接口以获取所需数据是不同的测试。
对于模拟接口,我更喜欢Rhino mocks。关于如何使用它,请参见here

b1payxdu

b1payxdu3#

Microsoft的HTTP堆栈在开发时没有考虑到单元测试和分离。
您有三个选项:

  • 使对web的调用尽可能小(即发送和取回数据并传递给其他方法),并测试其余的。就web调用而言,应该有很多神奇的事情发生,而且非常简单。
  • 在另一个类中 Package HTTP调用,并在测试时传递模拟对象。
  • 用另外两个类 Package HttpWebResponseHttpWebRequest,这就是MVC团队对HttpContext所做的。

第二种选择:

interface IWebCaller
{
    string CallWeb(string address);
}
aiazj4mn

aiazj4mn4#

如果有帮助,请在下面找到使用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);
}

单元测试运行并成功通过。
我一直在寻找一段时间如何有效地做到这一点的答案。

093gszye

093gszye5#

实际上,你可以返回HttpWebResponse而不需要模仿,请看我的回答,它不需要任何“外部”代理接口,只需要“标准”的WebRequestWebResponseICreateWebRequest
如果您不需要访问HttpWebResponse,而只需要处理WebResponse,那就更容易了;我们在单元测试中这样做是为了返回“预制的”内容响应以供使用。为了返回实际的HTTP状态代码,我不得不“走额外的路”,以模拟例如404个响应,这需要您使用HttpWebResponse,以便您可以访问StatusCode属性等。
其他解决方案假设所有内容都是HttpWebXXX,忽略了WebRequest.Create()支持的所有内容,除了HTTP,HTTP可以是您想要使用的任何注册前缀的处理程序(通过WebRequest.RegisterPrefix(),如果您忽略了这一点,您就错过了,因为这是一种很好的方式来公开您无法访问的其他内容流,例如嵌入式资源流、文件流、等等。
此外,显式地将WebRequest.Create()的返回强制转换为HttpWebRequest
到breakage的路径**,因为方法的返回类型是WebRequest,这再次表明对API实际工作方式的一些无知。

sbdsn5lh

sbdsn5lh6#

虽然有一个公认的答案,我想分享我的观点。有很多时候,修改源代码是不是一个选项的建议。此外,答案引入了至少可以说很多生产代码。
因此,我更喜欢使用免费的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");
        }
rkkpypqq

rkkpypqq7#

我在这个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;
    }
}

相关问题