代码之家  ›  专栏  ›  技术社区  ›  Eonasdan

测试HttpModule wtih响应。过滤器和响应。写

  •  1
  • Eonasdan  · 技术社区  · 8 年前

    我正在开发一个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 获取moq 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

    1 回复  |  直到 8 年前
        1
  •  4
  •   CodeFuller    8 年前

    让我们看看中的以下语句 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是一个伪对象。其默认实现 写入() 方法不执行任何操作,传递的数据将丢失。

    要解决此问题,可以在上设置回调 Response mock,将传递的数据写入流,然后在读取时返回。实际上你离它很近。

    在里面 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;
        });
    
        // ...
    }
    

    还应删除设置 inputStream 定位到 0 在里面 MoqHttpContextExtensions.StreamWrite() ,以便您写入的html数据 UnitTest1.TestMethod1() 追加,而不是覆盖:

    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. 您有以下签入 单元ApplicationEndRequest() 方法:

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

      执行UT时, context.Response.Filter 是一个 MemoryStream 不是一个 ResponseSniffer . 调用的Setter Module 建造商:

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

      实际上不会影响返回的值 滤器 getter,因为它是一个模拟,当前总是返回 内存流 安装时使用的 SetupGet . 要解决此问题,您应该实际模拟属性行为:保存传递给setter的值并在getter中返回它。以下是的最终设置 回答滤器 属性:

      Stream filter = new MemoryStream();
      MockResponse.SetupSet(response => response.Filter = It.IsAny<Stream>()).Callback<Stream>(value => filter = value);
      MockResponse.SetupGet(response => response.Filter).Returns(() => filter);
      

      确保已删除的所有其他模拟 回答滤器 所有物

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

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

      但是 PreRequestHandlerExecute 集合 Response.Filter 以实例 响应Sniffer . 那么什么时候 httpContext.StreamWrite 上面叫做, httpContext.Response.Filter 实际持有的实例 内存流 响应Sniffer . 因此,您应该做的最后一个修复是更改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应该可以工作。然而,当前的测试非常繁琐。事实证明,要花这么多时间才能理解为什么它不起作用。这样的测试很难维护和修复,随着时间的推移,它们会成为一种真正的痛苦。

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

    您应该强烈考虑重新设计当前测试。好的开始是对 响应Sniffer 单元 课程。

    最有价值的测试 响应Sniffer 用于验证写入数据是否已在中注册 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);
        }
    }
    

    关于 单元 类中,应进行以下几项检查:

    • PreRequestHandlerExecute() 集合 回答滤器 具有的实例 响应Sniffer .
    • 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 还有很多嘲笑和帮助。另一个好处是,如果某些测试被破坏,则更容易确定根本原因并加以修复。

    如果您是单元测试新手,并且正在寻找关于它的良好信息源,我强烈建议您阅读这本书 The Art of Unit Testing 罗伊·奥舍洛夫。