在自定义中间件中测试 response.WriteAsync()
Testing response.WriteAsync() in custom middleware
我有一个 ASP.NET 核心 API,我为此编写了自定义中间件,以便我可以处理异常并在一个位置写入日志。当通过 Kestrel 调试并从浏览器或邮递员提交请求时,中间件按要求工作,但在我的测试中,响应主体始终是空流。
下面是中间件 class 和我写的测试, 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);
}
}
}
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();
}
}
}
欢迎来到 Stack Overflow!
您的响应主体为空,因为您正在写入 NullStream
(不要与 null
值混淆)。
"A Stream with no backing store. Use Null to redirect output to a stream that will not consume any operating system resources. When the methods of Stream that provide writing are invoked on Null, the call simply returns, and no data is written. Null also implements a Read method that returns zero without reading data." - Docs
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();
不要忘记在单元测试结束时添加一些断言。毕竟,您想执行一些检查,对吗?
Assert.IsFalse(string.IsNullOrEmpty(text));
编辑#1
正如@nkosi 指出的那样,除非你有充分的理由,否则你应该始终使用 await
关键字调用异步方法:
await exceptionHandler.Invoke(context);
并用 async
标记方法定义并使其 return a Task
:
public async Task HandleException()
这样你就可以避免 deadlocks。
还值得指出(但不是必需)的是测试 classes 的命名约定。显然,您可以随意命名它,但请记住,当您的测试 class 与您要测试的 class 同名时,您最终会出现不必要的名称歧义。当然你可以用命名空间写全名(就像你做的那样),但以我懒惰的本性,这太多了所以我使用不同的名称来测试 class,例如 ExceptionHandlerTests
.
我有一个 ASP.NET 核心 API,我为此编写了自定义中间件,以便我可以处理异常并在一个位置写入日志。当通过 Kestrel 调试并从浏览器或邮递员提交请求时,中间件按要求工作,但在我的测试中,响应主体始终是空流。
下面是中间件 class 和我写的测试, 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);
}
}
}
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();
}
}
}
欢迎来到 Stack Overflow!
您的响应主体为空,因为您正在写入 NullStream
(不要与 null
值混淆)。
"A Stream with no backing store. Use Null to redirect output to a stream that will not consume any operating system resources. When the methods of Stream that provide writing are invoked on Null, the call simply returns, and no data is written. Null also implements a Read method that returns zero without reading data." - Docs
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();
不要忘记在单元测试结束时添加一些断言。毕竟,您想执行一些检查,对吗?
Assert.IsFalse(string.IsNullOrEmpty(text));
编辑#1
正如@nkosi 指出的那样,除非你有充分的理由,否则你应该始终使用 await
关键字调用异步方法:
await exceptionHandler.Invoke(context);
并用 async
标记方法定义并使其 return a Task
:
public async Task HandleException()
这样你就可以避免 deadlocks。
还值得指出(但不是必需)的是测试 classes 的命名约定。显然,您可以随意命名它,但请记住,当您的测试 class 与您要测试的 class 同名时,您最终会出现不必要的名称歧义。当然你可以用命名空间写全名(就像你做的那样),但以我懒惰的本性,这太多了所以我使用不同的名称来测试 class,例如 ExceptionHandlerTests
.