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

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

我希望停止实际上打击这个服务器,并使用 Moq(或任何 Mocking 库,如注入等)劫持和强制返回结果。

这可能吗?

下面是一些示例代码:-

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..
}

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

I was thinking that maybe I need to pass into this method (or a property of the class?) a mocked httpWebResponse or something but wasn't too sure if this was the way. Also, the response is a output from an httpWebRequest.GetResponse() method .. so maybe I just need to pass in a mocked HttpWebRequest ?.

任何建议与一些示例代码将是最受欢迎的!

52015 次浏览

与其嘲笑 HttpWebResponse,不如将调用封装在一个接口后面,并嘲笑该接口。

如果您正在测试 Web 响应是否打到了我想要的站点,这是一个不同的测试,如果类 A 调用 WebResponse 接口以获得所需的数据。

对于模拟界面,我更喜欢 犀牛嘲笑。请参阅如何使用它的 给你

在开发微软的 HTTP 堆栈时,没有一个考虑到了单元测试和分离。

你有三个选择:

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

第二个选择:

interface IWebCaller
{
string CallWeb(string address);
}

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

更新: 重访

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

在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没有嘲笑,看我的答案 给你。它不需要任何“外部”代理接口,只需要“标准”WebRequest WebResponseICreateWebRequest

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

其他的解决方案假设一切都是 HttpWebXXX 忽略除 HTTP 之外 WebRequest.Create()支持的所有内容,它可以作为一个处理程序为任何注册前缀你关心使用(通过 WebRequest.RegisterPrefix(),如果你忽略它,你是错过了,因为它是一个伟大的方式来暴露其他内容流,否则你没有办法访问,例如嵌入式资源流,文件流等。

此外,显式地将 WebRequest.Create()的返回值强制转换为 HttpWebRequest一条通往破坏的道路,因为方法返回类型是 WebRequest,这再次表明了对该 API 实际工作方式的一些忽略。

我在 博客文章中找到了一个很好的解决方案:

它非常容易使用,你只需要这样做:

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;
}
}

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

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);
}

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

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

虽然有一个公认的答案,我想分享我的看法。很多时候,修改源代码并不是建议的选项。而且答案至少介绍了很多生产代码。

因此,我更喜欢使用免费的 和声 Lib并通过拦截 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");
}