【问题标题】:Testing HttpModule wtih Response.Filter and Response.Write使用 Response.Filter 和 Response.Write 测试 HttpModule
【发布时间】:2018-02-20 15:00:30
【问题描述】:

我正在开发一个 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 中设置,响应主体是我从测试设置中初始化的。

我尝试了一些选项(并阅读了很多东西),但这些似乎都不起作用:

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
}

我意识到测试需要断言而不是断点。我也意识到测试应该被分解一下。

代码仓库

Github

【问题讨论】:

  • 我建议从一次测试一种方法开始。如果方法只是在响应中添加过滤器,则执行该方法并验证是否已添加过滤器。测试的最后一步不应该是断点。它应该是实际检查输出并测试某些预期值的东西。这样你就不需要打断点——你可以一次又一次地运行测试。您可能会发现很多甚至大部分设置代码都可以在测试之间共享。
  • 是的,我知道测试并不完美,我也知道我不需要断点,除了我正在尝试调试测试。我知道我需要单独的测试,但如果不能作为一个整体工作,它是否适用于每个方法都没关系
  • 是的,但是单独的测试会隔离出整体的哪一部分不起作用。
  • 该模块有效。不起作用的是设置 Response.Filter 并执行 Response.Write。测试前后的输出流是一样的。在测试中,PreRequestHandlerExecute Filter 仍然是 Stream 而不是 ResponseSniffer
  • 你能分享一下项目和相关的类吗(例如在 GitHub 上)?

标签: c# integration-testing moq


【解决方案1】:

让我们看看Module.ApplicationEndRequest()方法中的以下语句:

context.Response.Write(builder.ToString());

从单元测试中执行此代码时,context.Response 是您在 MoqHttpContext.CreateBaseMocks() 中设置的模拟:

MockResponse = new Mock<HttpResponseBase>();
// ...
MockContext.Setup(ctx => ctx.Response).Returns(MockResponse.Object);

您不能指望您在模拟上调用Write() 方法,然后可以读回相同的数据。 Mock 是一个假对象。 Write() 方法的默认实现什么都不做,传递的数据只是丢失了。

要解决此问题,您可以在 Response 模拟上设置一个回调,它将传递的数据写入流,然后在读取时将其返回。你其实很接近它。

MoqHttpContext 类中声明一个流,您将在其中保存数据:

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 的过滤器的正确工作。

  1. 您尝试了一些模拟 Filter 属性的选项,但似乎没有一个是正确的。使用 Moq 模拟属性 getter 的正确方法是:

    MockResponse.SetupGet(response => response.Filter).Returns(filter);
    

    删除所有其他用于模拟response.Filter 的语句,但不要添加上述语句,这不是最终版本。

  2. 您在Module.ApplicationEndRequest() 方法中有以下检查:

    if (context.Response.Filter is ResponseSniffer filter)
    {
        // ...
    

    当执行 UT 时,context.Response.FilterMemoryStream 而不是 ResponseSniffer。在 Module 构造函数中调用的 Setter:

    context.Response.Filter = new ResponseSniffer(context.Response.Filter);
    

    实际上不会影响Filter getter 返回的值,因为它是一个模拟,当前总是返回您使用SetupGet 设置的MemoryStream 的实例。要解决此问题,您实际上应该模拟属性行为:保存传递给 setter 的值并在 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 属性的所有其他模拟。

  3. 您应该解决的最后一个问题 - 是来自 UT 的 Module 调用的顺序。目前顺序如下:

    httpContext.StreamWrite(httpContext.Response.Filter, html);
    // ...
    var module = new Module();
    module.PreRequestHandlerExecute(mockHttpContext.HttpContext());
    

    但是PreRequestHandlerExecuteResponse.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 应该可以工作了。但是目前的测试非常繁琐。需要花费大量时间才能理解它为什么不起作用的事实证明了这一点。这样的测试很难维护和修复,随着时间的推移它们变得非常痛苦。

此外,它更像是集成测试而不是单元测试,因为它调用了几个具有不同功能的类 - ResponseSnifferModule

您应该强烈考虑重新设计当前测试。好的开始是对ResponseSnifferModule 类进行单独测试。

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 类,应该进行几项检查:

  • PreRequestHandlerExecute()Response.Filter 设置为ResponseSniffer 的实例。
  • 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"));
    }
}

如您所见,测试非常简单明了。您不再需要那些带有大量模拟和助手的MoqHttpContextMoqHttpContextExtensions。另一个好处 - 如果某些测试被破坏,则更容易确定根本原因并修复它。

如果您不熟悉单元测试并且正在寻找有关它的良好信息来源,我强烈建议您阅读 Roy Osherove 的书 The Art of Unit Testing

【讨论】:

  • StreamWrite 是来自 o2platform 链接的扩展方法。看起来我很接近。感谢您,我确实让ApplicationEndRequest 参与了测试。但是,您的回答并没有解决context.Response.Filter = 的问题,我假设这是一个类似的问题,但是在 moq 中设置属性是什么样的?
  • 我已经完成了 MockResponse.SetupSet(response =&gt; response.Filter = It.IsAny&lt;Stream&gt;())MockResponse.SetupSet(response =&gt; response.Filter = It.IsAny&lt;ResponseSniffer&gt;()) 但这似乎仍然缺少一些东西
  • 非常感谢!我去看看那本书。我参与的很多项目一直都是“测试不是很好吗”,但没有太多,哈哈。我可以看到MoqHttpContext 的优势,但是由于这是一个简单的模块,因此可能不需要它。
猜你喜欢
  • 1970-01-01
  • 2016-06-25
  • 2011-02-03
  • 2011-06-23
  • 2023-03-24
  • 2013-05-09
  • 2015-10-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多