在自定义中间件中测试 response.WriteAsync()

Chr*_*sMB 9 c# mstest asp.net-core

我有一个 ASP.NET Core API,我为它编写了自定义中间件,以便我可以在一个地方处理异常和写入日志。当通过 Kestrel 调试并从浏览器或邮递员提交请求时,中间件按要求工作,但是在我的测试中,响应正文始终是空流。

下面是我编写的中间件类和测试,context.Response.WriteAsync(result) 由于某种原因似乎没有刷新流,但我不知道为什么。有谁能解释一下吗?

using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.IO;

namespace APP.API.Middleware
{
    public class ExceptionHandler
    {
        private readonly RequestDelegate request;
        private readonly ILogger logger;

        public ExceptionHandler(RequestDelegate request, ILogger<ExceptionHandler> logger)
        {
            this.request = request;
            this.logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await request(context);
            }
            catch (Exception ex)
            {
                await HandleExceptionAsync(context, ex);
            }
        }

        private Task HandleExceptionAsync(HttpContext context, Exception ex)
        {
            HttpStatusCode statusCode = HttpStatusCode.InternalServerError;
            logger.LogError(ex, "Fatal exception");

            var result = JsonConvert.SerializeObject(new { error = ex.Message });
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)statusCode;

            return context.Response.WriteAsync(result);
        }
    }
}

Run Code Online (Sandbox Code Playgroud)
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace APP.Testing.Middleware
{
    [TestClass]
    public class ExceptionHandler
    {
        [TestMethod]
        public void HandleException()
        {
            var exceptionHandler = new API.Middleware.ExceptionHandler((innerHttpContext) =>
            {
                throw new System.Exception("Test exception");
            }, new NullLogger<API.Middleware.ExceptionHandler>());

            var context = new DefaultHttpContext();

            exceptionHandler.Invoke(context).Wait();

            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var reader = new StreamReader(context.Response.Body);
            var text = reader.ReadToEnd();

        }
    }
}

Run Code Online (Sandbox Code Playgroud)

Pro*_*log 13

欢迎使用堆栈溢出!

您的响应正文是空的,因为您正在写入 a NullStream(不要与nullvalue混淆)。

“一个没有后备存储的流。使用 Null 将输出重定向到一个不会消耗任何操作系统资源的流。当在 Null 上调用提供写入的 Stream 方法时,调用简单地返回,并且没有写入数据。Null还实现了一个 Read 方法,该方法在不读取数据的情况下返回零。” -文档

Body属性的默认值HttpResponse正是NullStream。在实际场景中,当 HTTP 请求到达时,NullStream替换为HttpResponseStream。您将无法自行使用它,因为它的可访问性级别设置为internal

解决方案

由于单元测试只是模拟真实场景,您可以将 替换为NullStream您想要的任何类型的流,例如MemoryStream

var exceptionHandler = new ExceptionHandler((innerHttpContext) =>
{
    throw new Exception("Test exception");
}, new NullLogger<ExceptionHandler>());

var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream(); // <== Replace the NullStream

await exceptionHandler.Invoke(context);

context.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(context.Response.Body);
var text = reader.ReadToEnd();
Run Code Online (Sandbox Code Playgroud)

不要忘记在单元测试结束时添加一些断言。毕竟,您想执行一些检查,对吗?

Assert.IsFalse(string.IsNullOrEmpty(text));
Run Code Online (Sandbox Code Playgroud)

编辑#1

正如@nkosi 指出的那样,除非你有一个很好的理由,否则你应该总是用await关键字调用异步方法:

await exceptionHandler.Invoke(context);
Run Code Online (Sandbox Code Playgroud)

并标记方法定义async并使其返回 a Task

public async Task HandleException()
Run Code Online (Sandbox Code Playgroud)

这样你就避免了死锁

还有一点值得指出(但不是必需的)是测试类的命名约定。显然,您可以随意命名它,但请记住,当您的测试类与您要测试的类具有相同的名称时,您最终会遇到不必要的名称歧义。当然,你可以用命名空间写全名(就像你所做的那样),但由于我的懒惰天性,这太多了,所以我使用不同的名称来测试类,例如ExceptionHandlerTests.