从WebRequest中嘲笑WebResponse
我终于开始创build一些与REST风格的Web界面一起工作的应用程序了,但是,我担心的是,每当我点击F5来运行一系列testing时,我都会敲击他们的服务器。
基本上,我需要得到一系列的networking响应,所以我可以testing我正确parsing不同的响应,而不是每次都打他们的服务器,我想我可以做一次,保存XML,然后在本地工作。
然而,我不明白我怎么可以“嘲笑”一个WebResponse,因为(AFAIK)他们只能通过WebRequest.GetResponse
你们怎么去嘲笑这种事情呢? 你做? 我真的不喜欢我锤击他们的服务器的事实:我不想更改代码太多 ,但我希望有一个这样做的优雅方式..
更新以下接受
威尔的回答是我需要的巴掌,我知道我错过了一个基本点!
- 创build一个接口,它将返回一个表示XML的代理对象。
- 实现接口两次,一个使用WebRequest,另一个返回静态“响应”。
- 接口implmentation然后根据响应实例化返回types,或者静态XML。
- 然后,您可以在testing或生产时将所需的类传递给服务层。
一旦我的代码被打倒,我会贴一些样本。
我发现这个问题,同时期待做同样的事情。 在任何地方找不到答案,但经过多一点挖掘发现,。NET框架内置支持这一点。
你可以用WebRequest.RegisterPrefix
注册一个工厂对象,当使用该前缀(或url)时, WebRequest.Create
将调用它。 工厂对象必须实现IWebRequestCreate
,它具有返回WebRequest
的单一方法Create
。 在这里你可以返回你的模拟WebRequest
。
我已经把一些示例代码放在http://blog.salamandersoft.co.uk/index.php/2009/10/how-to-mock-httpwebrequest-when-unit-testing/
这是一个不需要嘲笑的解决scheme。 您可以实现WebRequest
所有三个组件: IWebRequestCreate
WebRequest
和WebResponse
。 见下文。 我的例子生成失败的请求(通过抛出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) { } }
你必须创build一个HttpWebResponse
子类,否则你不能创build一个。
棘手的部分(在GetException()
方法中)是馈送你不能重写的值,例如StatusCode
,这是我们最好的朋友SerializaionInfo
进来的地方! 这是您提供您无法覆盖的值的位置。 显然,覆盖你能够的部分( HttpWebResponse
),以获得其余的方式。
我如何获得所有这些AddValue()
调用中的“名称”? 从exception消息! 我很高兴地告诉我每一个人,直到我开心。
现在,编译器会抱怨“过时”,但是这种方法很有效,包括.NET Framework版本4。
这是一个(通过)testing用例供参考:
[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; } }
你不能。 最好的办法是将其包装在代理对象中,然后嘲笑它。 或者,你必须使用一个模拟框架,可以拦截不能被模拟的types,比如TypeMock。 但是你在谈论雄鹿。 最好做一个小包装。
显然你可以做一些额外的工作。 在这里检查最高票数的答案。
我之前发现了以下博客,这个博客解释了使用Microsoft Moles的一个很好的方法。
http://maraboustork.co.uk/index.php/2011/03/mocking-httpwebresponse-with-moles/
总之解决schemebuild议如下:
[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."); }
这不是一个完美的解决scheme,但它之前为我工作,值得特别关注简单性:
HTTPSimulator
在typemock论坛中还有一个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); } } }