精心设计的 OWIN 中间件组件的成分(第 3 部分)

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

在我的 上一篇文章 中,我谈到了将 OWIN 管道的构造与 OWIN 中间件组件的定义分离。在这篇文章中,我将讨论下一个成分,即测试故事。

要素 4:测试整个 HTTP 管道

在大多数构建 ASP.NET MVC 或 WebAPI 控制器的项目中,我观察到两种测试策略:

  1. 通过传入强类型消息对象并分析生成的对象来直接测试控制器类。
  2. 保持控制器代码尽可能薄,并测试底层服务代码。

曾经有一个 WebAPI 自托管,您可以在单元测试中使用它,但它仍然需要一个实际的网络端口。虽然速度很慢,但它可以工作,前提是您不并行运行单元测试(XUnit 2 的默认设置)。

OWIN 的美妙之处在于它不直接依赖于实际的网络堆栈。它只是定义了一个基于简单类型(如字典、任务和函数)的抽象。提供 IIS 主机、控制台主机甚至 Windows 服务主机的是 Microsoft 的 Katana。因此,理想情况下,您的单元测试可以覆盖整个 OWIN 管道,而无需真正的网络端口,并且在与其他单元测试并行运行时也没有任何限制。如果您使用 OWIN,我编写了一个单元测试,断言 Piercer 在像这样从单元测试运行程序 AppDomain 内部运行时返回其自己的程序集。


 [Fact]
public async Task When_specifying_an_explicit_route_it_should_be_reachable_through_that_route()
{
    var appBuilder = new AppBuilder();
    appBuilder.UsePiercer(new PiercerSettings().AtRoute("/myroute"));
    AppFunc app = appBuilder.Build();
var httpClient = new HttpClient(new OwinHttpMessageHandler(appFunc));

var result = await httpClient
  .GetStringAsync("http://localhost/myroute/piercer/assemblies");

result.Should().Contain("Piercer.Middleware");

}

使用独立的 AppBuilder 允许您构建可通过 AppFunc 访问的内存中 OWIN 管道。要将实际的 HTTP 请求从典型的 HttpClient 对象发送到该管道,您可以使用 Damian Hickey 创建的漂亮的小库 OwinHttpMessageHandler 。它会将 HttpClient 客户端发送的 HttpRequestMessage 对象转换为 AppFunc 期望的字典,所有这些都不会触及网络堆栈。您可以 在此处 查看其实现。

因此,假设您正在编写一个需要与外界通信的中间件组件。我假设您可能会向中间件的设置类(如 PiercerSettings)添加一个属性或方法,使用 URL 进行通信。好吧,不要那样做。相反,要么采用 HttpClient 或 Uri 以及可选的 HttpMessageHandler 实例。如果您使用可选的 HttpMessageHandler,调用者可以传入要连接的实际 URL 或前面提到的 OwinHttpMessageHandler,如下所示:


 [Fact]
public async Task When_specifying_an_explicit_route_it_should_be_reachable_through_that_route()
{
    var appBuilder = new AppBuilder();
    appBuilder.UsePiercer(new PiercerSettings().AtRoute("/myroute"));
    AppFunc app = appBuilder.Build();
var httpClient = new HttpClient(new OwinHttpMessageHandler(appFunc));

var result = await httpClient
  .GetStringAsync("http://localhost/myroute/piercer/assemblies");

result.Should().Contain("Piercer.Middleware");

}

现在假设在您的单元测试中,您要连接的组件也应该托管在同一个 AppBuilder 上。在完成构建管道之前,您不能传入 AppFunc,因此我们在这里处于一种对峙状态。您可以通过延迟执行 lambda 表达式来解决这个问题。只需重新定义 ConnectingTo 方法,使其采用 Func:


 [Fact]
public async Task When_specifying_an_explicit_route_it_should_be_reachable_through_that_route()
{
    var appBuilder = new AppBuilder();
    appBuilder.UsePiercer(new PiercerSettings().AtRoute("/myroute"));
    AppFunc app = appBuilder.Build();
var httpClient = new HttpClient(new OwinHttpMessageHandler(appFunc));

var result = await httpClient
  .GetStringAsync("http://localhost/myroute/piercer/assemblies");

result.Should().Contain("Piercer.Middleware");

}

唯一需要注意的是,在完全构建 OWIN 管道之前,您的中间件组件不应尝试访问其他服务。如果在您的组件接收到它的第一个 HTTP 请求之前没有发生这种情况,那么您会没事的。但是,如果您的组件正在执行某种后台处理,则您必须明确地延迟它,我将在下一个成分中讨论。