使用 Response.Filter 和 Response.Write 测试 HttpModule
Testing HttpModule wtih Response.Filter and Response.Write
我正在开发一个 Http 模块,它只记录响应时间和大小,然后将结果附加到响应主体。
我的模块看起来像这样:
public override void PreRequestHandlerExecute(HttpContextBase context)
{
// Add a filter to capture response stream
context.Response.Filter = new ResponseSniffer(context.Response.Filter);
}
public override void ApplicationEndRequest(HttpContextBase context)
{
....
context.Response.Write(builder.ToString());
}
我现在想对该模块进行单元测试。我对单元测试很陌生。我已经改编了 o2platform 中的代码以获取最小起订量 httpcontext,并且目前为止有效。然而,响应过滤器似乎确实在 Pre 中设置,响应主体是我从测试设置中初始化的。
我尝试了一些选项(并阅读了很多资料),但其中 none 似乎有效:
public Mock<HttpResponseBase> MockResponse { get; set; }
...
var outputStream = new MemoryStream();
var filter = new MemoryStream();
//MockResponse.Setup(response => response.OutputStream).Returns(GetMockStream(outputStream).Object);
//MockResponse.Setup(response => response.Filter).Returns(GetMockStream(filter).Object);
MockResponse.Setup(response => response.OutputStream).Returns(() => outputStream);
//MockResponse.SetupSet(response => response.OutputStream = It.IsAny<Stream>()).Returns(() => outputStream);
MockResponse.Setup(response => response.Filter).Returns(() => filter);
MockResponse.SetupSet(response => response.Filter = It.IsAny<Stream>());
MockResponse.SetupSet(response => response.Filter = It.IsAny<ResponseSniffer>());
测试方法
[TestMethod]
public void TestMethod1()
{
var mockHttpContext = new MoqHttpContext();
var httpContext = mockHttpContext.HttpContext();
var html = @"<html>
<head></head>
<body>
<h1>Hello World</h1>
</body>
</html>";
httpContext.ResponseWrite(html);
httpContext.StreamWrite(httpContext.Response.Filter, html);
var module = new Module();
module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
module.ApplicationBeginRequest(mockHttpContext.HttpContext());
module.ApplicationEndRequest(mockHttpContext.HttpContext());
var responseRead = httpContext.ResponseRead(); //extension method to get output stream
var b = 1; //put breakpoint here
}
我意识到测试需要断言而不是断点。我也意识到测试应该被分解一下。
代码库
让我们看看 Module.ApplicationEndRequest()
方法中的以下语句:
context.Response.Write(builder.ToString());
当此代码从单元测试执行时,context.Response
是您在 MoqHttpContext.CreateBaseMocks():
中设置的模拟
MockResponse = new Mock<HttpResponseBase>();
// ...
MockContext.Setup(ctx => ctx.Response).Returns(MockResponse.Object);
您不能指望您在 mock 上调用 Write()
方法然后可以读回相同的数据。 Mock 是一个假对象。它的默认实现 Write()
方法什么都不做,传递的数据只是丢失。
要解决此问题,您可以在 Response
模拟上设置一个回调,它将传递的数据写入流,然后 return 在读取时返回。你其实已经很接近了
在 MoqHttpContext
class 中声明一个流,您将在其中保存数据:
public class MoqHttpContext
{
private readonly MemoryStream _outputStream = new MemoryStream();
// ...
}
然后在 CreateBaseMocks()
方法中设置回调:
public MoqHttpContext CreateBaseMocks()
{
// ...
MockResponse = new Mock<HttpResponseBase>();
MockResponse.Setup(x => x.Write(It.IsAny<string>())).Callback<string>(s =>
{
var data = Encoding.ASCII.GetBytes(s);
_outputStream.Write(data, 0, data.Length);
_outputStream.Flush();
_outputStream.Position = 0;
});
// ...
}
您还应该删除在 MoqHttpContextExtensions.StreamWrite()
中将 inputStream
位置设置为 0
的行,以便您在 UnitTest1.TestMethod1()
中写入的 html 数据是附加,未覆盖:
public static HttpContextBase StreamWrite(this HttpContextBase httpContextBase, Stream inputStream, string text)
{
if (inputStream == null) inputStream = new MemoryStream();
var streamWriter = new StreamWriter(inputStream);
inputStream.Position = inputStream.Length;
streamWriter.Write(text);
streamWriter.Flush();
// Remove this line
//inputStream.Position = 0;
return httpContextBase;
}
就是这样。现在如果你在测试中检查 responseRead
的值,你会看到 Http 模块附加的数据在那里。
更新(修复过滤器问题)
当前代码存在 3 个不同的问题,导致 UT 的过滤器无法正常工作。
您尝试了一些选项来模拟 Filter
属性,但是其中 none 似乎是正确的。使用 Moq 模拟 属性 getter 的正确方法是:
MockResponse.SetupGet(response => response.Filter).Returns(filter);
删除所有其他用于模拟的语句 response.Filter
,但不要添加上面的语句,它不是最终版本。
您有以下签入 Module.ApplicationEndRequest()
方法:
if (context.Response.Filter is ResponseSniffer filter)
{
// ...
执行 UT 时,context.Response.Filter
是 MemoryStream
而不是 ResponseSniffer
。 Setter 在 Module
构造函数中调用:
context.Response.Filter = new ResponseSniffer(context.Response.Filter);
实际上不会影响 return 由 Filter
getter 编辑的值,因为它是一个模拟,当前总是 return 您设置的 MemoryStream
实例SetupGet
。要解决此问题,您实际上应该效仿 属性 行为:将传递给 setter 和 return 的值保存在 getter 中。这是 response.Filter
属性:
的最终设置
Stream filter = new MemoryStream();
MockResponse.SetupSet(response => response.Filter = It.IsAny<Stream>()).Callback<Stream>(value => filter = value);
MockResponse.SetupGet(response => response.Filter).Returns(() => filter);
确保您已删除 response.Filter
属性.
的所有其他模拟
您应该解决的最后一个问题是来自 UT 的 Module
调用顺序。目前顺序如下:
httpContext.StreamWrite(httpContext.Response.Filter, html);
// ...
var module = new Module();
module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
但是 PreRequestHandlerExecute
将 Response.Filter
设置为 ResponseSniffer
的实例。所以当上面的 httpContext.StreamWrite
被调用时,httpContext.Response.Filter
实际上持有 MemoryStream
的实例,而不是 ResponseSniffer
。因此,您应该做的最后一个修复是更改 UT 正文中语句的顺序:
// ...
var module = new Module();
module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
httpContext.ResponseWrite(html);
httpContext.StreamWrite(httpContext.Response.Filter, html);
module.ApplicationBeginRequest(mockHttpContext.HttpContext());
module.ApplicationEndRequest(mockHttpContext.HttpContext());
// ...
更新(UT 重新设计)
此时您的 UT 应该可以工作了。然而目前的测试非常繁琐。需要花费大量时间来理解为什么它不起作用这一事实证明了这一点。这样的测试很难维护和修复,随着时间的推移,它们会变成真正的痛苦。
此外,它更像是集成测试而不是单元测试,因为它调用了几个具有不同功能的 classes - ResponseSniffer
和 Module
.
您应该强烈考虑重新设计当前测试。好的开始是对 ResponseSniffer
和 Module
classes 进行单独测试。
ResponseSniffer
最有价值的测试是验证写入数据是否已在 RecordStream
中注册的测试:
[TestClass]
public class ResponseSnifferTests
{
[TestMethod]
public void Write_WritesDataToRecordStream()
{
// Arrange
var inData = new byte[] { 0x01 };
var target = new ResponseSniffer(Mock.Of<Stream>());
// Act
target.Write(inData, 0, inData.Length);
// Assert
target.RecordStream.Position = 0;
var outData = new byte[inData.Length];
int outSize = target.RecordStream.Read(outData, 0, outData.Length);
Assert.AreEqual(inData.Length, outSize);
CollectionAssert.AreEqual(inData, outData);
}
}
关于Module
class,有几项检查应该做:
PreRequestHandlerExecute()
使用 ResponseSniffer
. 的实例设置 Response.Filter
ApplicationBeginRequest()
添加 Stopwatch
到 context.Items
字典。
ApplicationEndRequest()
将请求信息写入响应。
UT 方法意味着在单独的测试中检查这些事实。以下是此类 3 项测试的样本:
[TestClass]
public class ModuleTests
{
[TestMethod]
public void PreRequestHandlerExecuteShouldSetResponseSnifferAsFilter()
{
// Arrange
Stream filter = null;
Mock<HttpResponseBase> httpResponseMock = new Mock<HttpResponseBase>();
httpResponseMock.SetupSet(response => response.Filter = It.IsAny<Stream>()).Callback<Stream>(value => filter = value);
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Response).Returns(httpResponseMock.Object);
var target = new Module();
// Act
target.PreRequestHandlerExecute(httpContextStub.Object);
// Assert
Assert.IsNotNull(filter);
Assert.IsInstanceOfType(filter, typeof(ResponseSniffer));
}
[TestMethod]
public void ApplicationBeginRequestShouldStoreStopwatchInContextItems()
{
// Arrange
var items = new Dictionary<string, object>();
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Items).Returns(items);
var target = new Module();
// Act
target.ApplicationBeginRequest(httpContextStub.Object);
// Assert
Assert.IsTrue(items.ContainsKey("X-ResponseTime"));
Assert.IsInstanceOfType(items["X-ResponseTime"], typeof(Stopwatch));
}
[TestMethod]
public void ApplicationEndRequestShouldAddRequestInfoToResponse()
{
// Arrange
Mock<HttpRequestBase> httpRequestMock = new Mock<HttpRequestBase>();
httpRequestMock.SetupGet(x => x.FilePath).Returns("/test");
string writtenData = null;
Mock<HttpResponseBase> httpResponseMock = new Mock<HttpResponseBase>();
httpResponseMock.Setup(x => x.Write(It.IsAny<string>())).Callback<string>(s => writtenData = s);
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Request).Returns(httpRequestMock.Object);
httpContextStub.SetupGet(x => x.Response).Returns(httpResponseMock.Object);
httpContextStub.SetupGet(x => x.Items).Returns(new Dictionary<string, object> { ["X-ResponseTime"] = new Stopwatch() });
var target = new Module();
// Act
target.ApplicationEndRequest(httpContextStub.Object);
// Assert
Assert.IsTrue(Regex.IsMatch(writtenData, @"Response Size: \d+ bytes<br/>"));
Assert.IsTrue(Regex.IsMatch(writtenData, @"Module request time: \d+ ms"));
}
}
如您所见,测试非常简单明了。您不再需要那些 MoqHttpContext
和 MoqHttpContextExtensions
以及大量模拟和助手。另一个好处 - 如果某些测试被破坏,则更容易确定根本原因并修复它。
如果您是单元测试的新手并且正在寻找有关它的良好信息来源,我强烈推荐 Roy Osherove 的书 The Art of Unit Testing。
我正在开发一个 Http 模块,它只记录响应时间和大小,然后将结果附加到响应主体。
我的模块看起来像这样:
public override void PreRequestHandlerExecute(HttpContextBase context)
{
// Add a filter to capture response stream
context.Response.Filter = new ResponseSniffer(context.Response.Filter);
}
public override void ApplicationEndRequest(HttpContextBase context)
{
....
context.Response.Write(builder.ToString());
}
我现在想对该模块进行单元测试。我对单元测试很陌生。我已经改编了 o2platform 中的代码以获取最小起订量 httpcontext,并且目前为止有效。然而,响应过滤器似乎确实在 Pre 中设置,响应主体是我从测试设置中初始化的。
我尝试了一些选项(并阅读了很多资料),但其中 none 似乎有效:
public Mock<HttpResponseBase> MockResponse { get; set; }
...
var outputStream = new MemoryStream();
var filter = new MemoryStream();
//MockResponse.Setup(response => response.OutputStream).Returns(GetMockStream(outputStream).Object);
//MockResponse.Setup(response => response.Filter).Returns(GetMockStream(filter).Object);
MockResponse.Setup(response => response.OutputStream).Returns(() => outputStream);
//MockResponse.SetupSet(response => response.OutputStream = It.IsAny<Stream>()).Returns(() => outputStream);
MockResponse.Setup(response => response.Filter).Returns(() => filter);
MockResponse.SetupSet(response => response.Filter = It.IsAny<Stream>());
MockResponse.SetupSet(response => response.Filter = It.IsAny<ResponseSniffer>());
测试方法
[TestMethod]
public void TestMethod1()
{
var mockHttpContext = new MoqHttpContext();
var httpContext = mockHttpContext.HttpContext();
var html = @"<html>
<head></head>
<body>
<h1>Hello World</h1>
</body>
</html>";
httpContext.ResponseWrite(html);
httpContext.StreamWrite(httpContext.Response.Filter, html);
var module = new Module();
module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
module.ApplicationBeginRequest(mockHttpContext.HttpContext());
module.ApplicationEndRequest(mockHttpContext.HttpContext());
var responseRead = httpContext.ResponseRead(); //extension method to get output stream
var b = 1; //put breakpoint here
}
我意识到测试需要断言而不是断点。我也意识到测试应该被分解一下。
代码库
让我们看看 Module.ApplicationEndRequest()
方法中的以下语句:
context.Response.Write(builder.ToString());
当此代码从单元测试执行时,context.Response
是您在 MoqHttpContext.CreateBaseMocks():
MockResponse = new Mock<HttpResponseBase>();
// ...
MockContext.Setup(ctx => ctx.Response).Returns(MockResponse.Object);
您不能指望您在 mock 上调用 Write()
方法然后可以读回相同的数据。 Mock 是一个假对象。它的默认实现 Write()
方法什么都不做,传递的数据只是丢失。
要解决此问题,您可以在 Response
模拟上设置一个回调,它将传递的数据写入流,然后 return 在读取时返回。你其实已经很接近了
在 MoqHttpContext
class 中声明一个流,您将在其中保存数据:
public class MoqHttpContext
{
private readonly MemoryStream _outputStream = new MemoryStream();
// ...
}
然后在 CreateBaseMocks()
方法中设置回调:
public MoqHttpContext CreateBaseMocks()
{
// ...
MockResponse = new Mock<HttpResponseBase>();
MockResponse.Setup(x => x.Write(It.IsAny<string>())).Callback<string>(s =>
{
var data = Encoding.ASCII.GetBytes(s);
_outputStream.Write(data, 0, data.Length);
_outputStream.Flush();
_outputStream.Position = 0;
});
// ...
}
您还应该删除在 MoqHttpContextExtensions.StreamWrite()
中将 inputStream
位置设置为 0
的行,以便您在 UnitTest1.TestMethod1()
中写入的 html 数据是附加,未覆盖:
public static HttpContextBase StreamWrite(this HttpContextBase httpContextBase, Stream inputStream, string text)
{
if (inputStream == null) inputStream = new MemoryStream();
var streamWriter = new StreamWriter(inputStream);
inputStream.Position = inputStream.Length;
streamWriter.Write(text);
streamWriter.Flush();
// Remove this line
//inputStream.Position = 0;
return httpContextBase;
}
就是这样。现在如果你在测试中检查 responseRead
的值,你会看到 Http 模块附加的数据在那里。
更新(修复过滤器问题)
当前代码存在 3 个不同的问题,导致 UT 的过滤器无法正常工作。
您尝试了一些选项来模拟
Filter
属性,但是其中 none 似乎是正确的。使用 Moq 模拟 属性 getter 的正确方法是:MockResponse.SetupGet(response => response.Filter).Returns(filter);
删除所有其他用于模拟的语句
response.Filter
,但不要添加上面的语句,它不是最终版本。您有以下签入
Module.ApplicationEndRequest()
方法:if (context.Response.Filter is ResponseSniffer filter) { // ...
执行 UT 时,
context.Response.Filter
是MemoryStream
而不是ResponseSniffer
。 Setter 在Module
构造函数中调用:context.Response.Filter = new ResponseSniffer(context.Response.Filter);
实际上不会影响 return 由
的最终设置Filter
getter 编辑的值,因为它是一个模拟,当前总是 return 您设置的MemoryStream
实例SetupGet
。要解决此问题,您实际上应该效仿 属性 行为:将传递给 setter 和 return 的值保存在 getter 中。这是response.Filter
属性:Stream filter = new MemoryStream(); MockResponse.SetupSet(response => response.Filter = It.IsAny<Stream>()).Callback<Stream>(value => filter = value); MockResponse.SetupGet(response => response.Filter).Returns(() => filter);
确保您已删除
response.Filter
属性. 的所有其他模拟
您应该解决的最后一个问题是来自 UT 的
Module
调用顺序。目前顺序如下:httpContext.StreamWrite(httpContext.Response.Filter, html); // ... var module = new Module(); module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
但是
PreRequestHandlerExecute
将Response.Filter
设置为ResponseSniffer
的实例。所以当上面的httpContext.StreamWrite
被调用时,httpContext.Response.Filter
实际上持有MemoryStream
的实例,而不是ResponseSniffer
。因此,您应该做的最后一个修复是更改 UT 正文中语句的顺序:// ... var module = new Module(); module.PreRequestHandlerExecute(mockHttpContext.HttpContext()); httpContext.ResponseWrite(html); httpContext.StreamWrite(httpContext.Response.Filter, html); module.ApplicationBeginRequest(mockHttpContext.HttpContext()); module.ApplicationEndRequest(mockHttpContext.HttpContext()); // ...
更新(UT 重新设计)
此时您的 UT 应该可以工作了。然而目前的测试非常繁琐。需要花费大量时间来理解为什么它不起作用这一事实证明了这一点。这样的测试很难维护和修复,随着时间的推移,它们会变成真正的痛苦。
此外,它更像是集成测试而不是单元测试,因为它调用了几个具有不同功能的 classes - ResponseSniffer
和 Module
.
您应该强烈考虑重新设计当前测试。好的开始是对 ResponseSniffer
和 Module
classes 进行单独测试。
ResponseSniffer
最有价值的测试是验证写入数据是否已在 RecordStream
中注册的测试:
[TestClass]
public class ResponseSnifferTests
{
[TestMethod]
public void Write_WritesDataToRecordStream()
{
// Arrange
var inData = new byte[] { 0x01 };
var target = new ResponseSniffer(Mock.Of<Stream>());
// Act
target.Write(inData, 0, inData.Length);
// Assert
target.RecordStream.Position = 0;
var outData = new byte[inData.Length];
int outSize = target.RecordStream.Read(outData, 0, outData.Length);
Assert.AreEqual(inData.Length, outSize);
CollectionAssert.AreEqual(inData, outData);
}
}
关于Module
class,有几项检查应该做:
PreRequestHandlerExecute()
使用ResponseSniffer
. 的实例设置 ApplicationBeginRequest()
添加Stopwatch
到context.Items
字典。ApplicationEndRequest()
将请求信息写入响应。
Response.Filter
UT 方法意味着在单独的测试中检查这些事实。以下是此类 3 项测试的样本:
[TestClass]
public class ModuleTests
{
[TestMethod]
public void PreRequestHandlerExecuteShouldSetResponseSnifferAsFilter()
{
// Arrange
Stream filter = null;
Mock<HttpResponseBase> httpResponseMock = new Mock<HttpResponseBase>();
httpResponseMock.SetupSet(response => response.Filter = It.IsAny<Stream>()).Callback<Stream>(value => filter = value);
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Response).Returns(httpResponseMock.Object);
var target = new Module();
// Act
target.PreRequestHandlerExecute(httpContextStub.Object);
// Assert
Assert.IsNotNull(filter);
Assert.IsInstanceOfType(filter, typeof(ResponseSniffer));
}
[TestMethod]
public void ApplicationBeginRequestShouldStoreStopwatchInContextItems()
{
// Arrange
var items = new Dictionary<string, object>();
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Items).Returns(items);
var target = new Module();
// Act
target.ApplicationBeginRequest(httpContextStub.Object);
// Assert
Assert.IsTrue(items.ContainsKey("X-ResponseTime"));
Assert.IsInstanceOfType(items["X-ResponseTime"], typeof(Stopwatch));
}
[TestMethod]
public void ApplicationEndRequestShouldAddRequestInfoToResponse()
{
// Arrange
Mock<HttpRequestBase> httpRequestMock = new Mock<HttpRequestBase>();
httpRequestMock.SetupGet(x => x.FilePath).Returns("/test");
string writtenData = null;
Mock<HttpResponseBase> httpResponseMock = new Mock<HttpResponseBase>();
httpResponseMock.Setup(x => x.Write(It.IsAny<string>())).Callback<string>(s => writtenData = s);
Mock<HttpContextBase> httpContextStub = new Mock<HttpContextBase>();
httpContextStub.SetupGet(x => x.Request).Returns(httpRequestMock.Object);
httpContextStub.SetupGet(x => x.Response).Returns(httpResponseMock.Object);
httpContextStub.SetupGet(x => x.Items).Returns(new Dictionary<string, object> { ["X-ResponseTime"] = new Stopwatch() });
var target = new Module();
// Act
target.ApplicationEndRequest(httpContextStub.Object);
// Assert
Assert.IsTrue(Regex.IsMatch(writtenData, @"Response Size: \d+ bytes<br/>"));
Assert.IsTrue(Regex.IsMatch(writtenData, @"Module request time: \d+ ms"));
}
}
如您所见,测试非常简单明了。您不再需要那些 MoqHttpContext
和 MoqHttpContextExtensions
以及大量模拟和助手。另一个好处 - 如果某些测试被破坏,则更容易确定根本原因并修复它。
如果您是单元测试的新手并且正在寻找有关它的良好信息来源,我强烈推荐 Roy Osherove 的书 The Art of Unit Testing。