模拟 WebRequest 的 WebResponse

36

我最近开始尝试创建一些与RESTful web接口配合的应用程序,不过我担心每次按F5运行一系列测试时都会对其服务器造成压力。

基本上,我需要获取一系列web响应以便测试是否正确解析了不同的响应,而不是每次都访问他们的服务器。因此,我想可以做一次这样的操作,保存XML文件,然后在本地工作。

但是,我不知道如何“mock”一个WebResponse,因为(据我所知)它们只能由WebRequest.GetResponse实例化。

你们如何处理这种mock情况?你们有这样做吗?我真的不喜欢自己在攻击他们的服务器:S 我不想改变代码太多,但我认为肯定有一种优雅的方法可以解决这个问题...

Update Following Accept

Will的答案给了我很大的启示,我知道自己忽略了一个根本性的点!

  • 创建一个接口,该接口将返回一个代表XML的代理对象。
  • 实现该接口两次,一次使用WebRequest,另一次返回静态“responses”。
  • 接口实现然后基于响应或静态XML来实例化返回类型。
  • 然后在测试或生产时将所需的类传递给服务层。

一旦我完成了代码,我就会贴一些样本。


嗨Rob,你搞定了这个吗?我刚刚发现这个问题,作为一个单元测试新手,我很想看看你是如何解决这个问题的。 - Nick
嗨,尼克,就像在更新中一样。我最终做的是针对接口进行编程。然后我实现了两次接口。一个返回静态内容,一个实际使用 WebRequest。然后您可以 UT 消费者,然后只需使用实际的安全知识测试了消费者类。永远记住,“不要测试别人的代码”(例如 WebRequest)。 - Rob Cooper
6个回答

60

我在寻找实现相同需求时发现了这个问题。 无法在任何地方找到答案,但是经过更深入的挖掘后发现,.Net Framework具有内建支持。

您可以使用WebRequest.RegisterPrefix方法注册一个工厂对象,当使用该前缀(或url)时,WebRequest.Create将调用该对象。 工厂对象必须实现IWebRequestCreate接口,该接口具有单个Create方法,该方法返回一个WebRequest对象。 在此处,您可以返回您的模拟WebRequest对象。

我在这里放置了一些示例代码。


7
这是正确的方法,直到你的客户端代码开始检查HttpWebResponse来查看状态码...那么你需要查看我下面的答案! - escape-llc
我仍然无法弄清楚的是如何处理连续的两个网络请求... - Nick

14

以下是一种不需要模拟的解决方案。您需要实现WebRequest的三个组件:IWebRequestCreateWebRequestWebResponse。请参见下面的示例。我的示例生成失败的请求(通过抛出WebException),但应该能够将其调整为发送“真实”的响应:

class WebRequestFailedCreate : IWebRequestCreate {
    HttpStatusCode status;
    String statusDescription;
    public WebRequestFailedCreate(HttpStatusCode hsc, String sd) {
        status = hsc;
        statusDescription = sd;
    }
    #region IWebRequestCreate Members
    public WebRequest Create(Uri uri) {
        return new WebRequestFailed(uri, status, statusDescription);
    }
    #endregion
}
class WebRequestFailed : WebRequest {
    HttpStatusCode status;
    String statusDescription;
    Uri itemUri;
    public WebRequestFailed(Uri uri, HttpStatusCode status, String statusDescription) {
        this.itemUri = uri;
        this.status = status;
        this.statusDescription = statusDescription;
    }
    WebException GetException() {
        SerializationInfo si = new SerializationInfo(typeof(HttpWebResponse), new System.Runtime.Serialization.FormatterConverter());
        StreamingContext sc = new StreamingContext();
        WebHeaderCollection headers = new WebHeaderCollection();
        si.AddValue("m_HttpResponseHeaders", headers);
        si.AddValue("m_Uri", itemUri);
        si.AddValue("m_Certificate", null);
        si.AddValue("m_Version", HttpVersion.Version11);
        si.AddValue("m_StatusCode", status);
        si.AddValue("m_ContentLength", 0);
        si.AddValue("m_Verb", "GET");
        si.AddValue("m_StatusDescription", statusDescription);
        si.AddValue("m_MediaType", null);
        WebResponseFailed wr = new WebResponseFailed(si, sc);
        Exception inner = new Exception(statusDescription);
        return new WebException("This request failed", inner, WebExceptionStatus.ProtocolError, wr);
    }
    public override WebResponse GetResponse() {
        throw GetException();
    }
    public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state) {
        Task<WebResponse> f = Task<WebResponse>.Factory.StartNew (
            _ =>
            {
                throw GetException();
            },
            state
        );
        if (callback != null) f.ContinueWith((res) => callback(f));
        return f;
    }
    public override WebResponse EndGetResponse(IAsyncResult asyncResult) {
        return ((Task<WebResponse>)asyncResult).Result;
    }

}
class WebResponseFailed : HttpWebResponse {
    public WebResponseFailed(SerializationInfo serializationInfo, StreamingContext streamingContext)
        : base(serializationInfo, streamingContext) {
    }
}

你必须创建一个HttpWebResponse子类,因为否则无法创建它。

复杂的部分(在GetException()方法中)是如何输入不能重写的值,例如StatusCode。这就是我们最好的伙伴SerializationInfo派上用场的地方!在这里,您可以提供无法重写的值。显然,重写HttpWebResponse的其余部分以使其达到预期效果。

我如何获取所有这些AddValue()调用中的“名称”?从异常消息中!异常消息很友好地按顺序告诉了我每个名称,直到我满意为止。

现在,编译器会抱怨“已过时”,但这仍然可以在.NET Framework版本4中使用。以下是一个(通过)的参考测试用例:

    [TestMethod, ExpectedException(typeof(WebException))]
    public void WebRequestFailedThrowsWebException() {
        string TestURIProtocol = TestContext.TestName;
        var ResourcesBaseURL = TestURIProtocol + "://resources/";
        var ContainerBaseURL = ResourcesBaseURL + "container" + "/";
        WebRequest.RegisterPrefix(TestURIProtocol, new WebRequestFailedCreate(HttpStatusCode.InternalServerError, "This request failed on purpose."));
        WebRequest wr = WebRequest.Create(ContainerBaseURL);
        try {
            WebResponse wrsp = wr.GetResponse();
            using (wrsp) {
                Assert.Fail("WebRequest.GetResponse() Should not have succeeded.");
            }
        }
        catch (WebException we) {
            Assert.IsInstanceOfType(we.Response, typeof(HttpWebResponse));
            Assert.AreEqual(HttpStatusCode.InternalServerError, (we.Response as HttpWebResponse).StatusCode, "Status Code failed");
            throw we;
        }
    }

1
这确实是一种很好的方法,用于模拟Web响应以进行单元测试,直到.NET 4.6为止。但在.NET Standard中不起作用,因为“已过时”的构造函数已被删除。 - desautelsj

2
我之前发现了一篇博客,其中介绍了使用Microsoft Moles的不错方法。
链接在此:http://maraboustork.co.uk/index.php/2011/03/mocking-httpwebresponse-with-moles/ 简而言之,该解决方案建议如下:
    [TestMethod]
    [HostType("Moles")]
    [Description("Tests that the default scraper returns the correct result")]
    public void Scrape_KnownUrl_ReturnsExpectedValue()
    {
        var mockedWebResponse = new MHttpWebResponse();

        MHttpWebRequest.AllInstances.GetResponse = (x) =>
        {
            return mockedWebResponse;
        };

        mockedWebResponse.StatusCodeGet = () => { return HttpStatusCode.OK; };
        mockedWebResponse.ResponseUriGet = () => { return new Uri("http://www.google.co.uk/someRedirect.aspx"); };
        mockedWebResponse.ContentTypeGet = () => { return "testHttpResponse"; }; 

        var mockedResponse = "<html> \r\n" +
                             "  <head></head> \r\n" +
                             "  <body> \r\n" +
                             "     <h1>Hello World</h1> \r\n" +
                             "  </body> \r\n" +
                             "</html>";

        var s = new MemoryStream();
        var sw = new StreamWriter(s);

            sw.Write(mockedResponse);
            sw.Flush();

            s.Seek(0, SeekOrigin.Begin);

        mockedWebResponse.GetResponseStream = () => s;

        var scraper = new DefaultScraper();
        var retVal = scraper.Scrape("http://www.google.co.uk");

        Assert.AreEqual(mockedResponse, retVal.Content, "Should have returned the test html response");
        Assert.AreEqual("http://www.google.co.uk/someRedirect.aspx", retVal.FinalUrl, "The finalUrl does not correctly represent the redirection that took place.");
    }

2
你无法直接进行mock,最好的方法是将其封装在代理对象中,然后对其进行mock。或者,您可以使用可以拦截无法进行mock的类型的mock框架,例如TypeMock。但这会花费很多成本。更好的方法是进行一些封装。
显然,你可以通过额外的工作来实现mock。请查看此处的最高投票答案。

你能否解释一下如何在不影响核心代码的情况下进行“包装”?一旦调用了GetResponse,它就会立即执行。显然,我需要进行一些依赖注入,但是我不知道如何获取到我所需的对象,你明白吗? - Rob Cooper
不要再说了,我错了。我同意你的想法。创建代理对象来携带结果,并为调用创建一个接口。在两个类中实现:1 = WebRequest,2 = 静态对象)。现在有意义了,谢谢你打醒我,让我看清事情的本质,我知道我之前看不到树木背后的森林 :) - Rob Cooper
2
这是不正确的,正如Richard Willis在下面的回答中展示的那样,它可以使用他链接到的代码进行模拟WebResponse。 - tmont
2
@tmont 很有趣,+1 给 Willis。编辑得很清晰。不错,他们隐藏得够好的... - user1228

0
你可以使用 NSubstitute,例如:
        var httpWebResponse = Substitute.For<HttpWebResponse>();
        httpWebResponse.StatusCode.Returns(HttpStatusCode.NotFound);
        httpWebResponse.StatusDescription.Returns("Not Found");

0

这不是一个完美的解决方案,但之前对我有效,并且由于其简单性而值得额外关注:

HTTP模拟器

还有一个在Typemock论坛中记录的示例:

using System;
using System.IO;
using System.Net;
using NUnit.Framework;
using TypeMock;

namespace MockHttpWebRequest
{
  public class LibraryClass
  {
    public string GetGoogleHomePage()
    {
      HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://www.google.com");
      HttpWebResponse response = (HttpWebResponse)request.GetResponse();
      using (StreamReader reader = new StreamReader(response.GetResponseStream()))
      {
        return reader.ReadToEnd();
      }
    }
  }

  [TestFixture]
  [VerifyMocks]
  public class UnitTests
  {
    private Stream responseStream = null;
    private const string ExpectedResponseContent = "Content from mocked response.";

    [SetUp]
    public void SetUp()
    {
      System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
      byte[] contentAsBytes = encoding.GetBytes(ExpectedResponseContent);
      this.responseStream = new MemoryStream();
      this.responseStream.Write(contentAsBytes, 0, contentAsBytes.Length);
      this.responseStream.Position = 0;
    }

    [TearDown]
    public void TearDown()
    {
      if (responseStream != null)
      {
        responseStream.Dispose();
        responseStream = null;
      }
    }

    [Test(Description = "Mocks a web request using natural mocks.")]
    public void NaturalMocks()
    {
      HttpWebRequest mockRequest = RecorderManager.CreateMockedObject<HttpWebRequest>(Constructor.Mocked);
      HttpWebResponse mockResponse = RecorderManager.CreateMockedObject<HttpWebResponse>(Constructor.Mocked);
      using (RecordExpectations recorder = RecorderManager.StartRecording())
      {
        WebRequest.Create("http://www.google.com");
        recorder.CheckArguments();
        recorder.Return(mockRequest);

        mockRequest.GetResponse();
        recorder.Return(mockResponse);

        mockResponse.GetResponseStream();
        recorder.Return(this.responseStream);
      }

      LibraryClass testObject = new LibraryClass();
      string result = testObject.GetGoogleHomePage();
      Assert.AreEqual(ExpectedResponseContent, result);
    }

    [Test(Description = "Mocks a web request using reflective mocks.")]
    public void ReflectiveMocks()
    {
      Mock<HttpWebRequest> mockRequest = MockManager.Mock<HttpWebRequest>(Constructor.Mocked);
      MockObject<HttpWebResponse> mockResponse = MockManager.MockObject<HttpWebResponse>(Constructor.Mocked);
      mockResponse.ExpectAndReturn("GetResponseStream", this.responseStream);
      mockRequest.ExpectAndReturn("GetResponse", mockResponse.Object);

      LibraryClass testObject = new LibraryClass();
      string result = testObject.GetGoogleHomePage();
      Assert.AreEqual(ExpectedResponseContent, result);
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接