在我的 上一篇文章 中,我谈到了将 OWIN 管道的构造与 OWIN 中间件组件的定义分离。在这篇文章中,我将讨论下一个成分,即测试故事。
要素 4:测试整个 HTTP 管道
在大多数构建 ASP.NET MVC 或 WebAPI 控制器的项目中,我观察到两种测试策略:
- 通过传入强类型消息对象并分析生成的对象来直接测试控制器类。
- 保持控制器代码尽可能薄,并测试底层服务代码。
曾经有一个 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 请求之前没有发生这种情况,那么您会没事的。但是,如果您的组件正在执行某种后台处理,则您必须明确地延迟它,我将在下一个成分中讨论。