【ASP.NET Core 沉思录】CreateWebHostBuilder 是一个 Convension
失蹤人口回歸。去年六月份開始,我開始翻譯一千多頁的《CSharp 7 in a Nutshell》到現在為止終于告一段落。我又回歸了表世界。從這次開始我希望展開一個全新的主題。我叫它 ASP.NET Core 沉思錄(多么高大上的名字,自我陶醉~)。今天是第一個主題。CreateWebHostBuilder 是一個 Convension。
太長不讀
對于 WebApplicationFactory<T> 而言,默認情況下會采取如下假定:
Startup 所在的程序集應當就是應用程序入口(Main)所在的程序集;(官方工程模板的坑)
應用程序入口所在的類(Program),里面會包含整個創建和配置 IWebHostBuilder 的過程;
創建和配置 IWebHostBuilder 的過程是由應用程序入口所在類的 CreateWebHostBuilder 方法完成的。
在滿足上述假定的情況下,無需額外代碼,Web 應用的執行和測試將共享相同的邏輯。如若不然,則測試失敗。如果無法滿足上述三種條件還可以通過集成 WebApplicationFactory<T> 并重寫 CreateWebHostBuilder 方法來解決。
以上約束僅僅限定于 WebApplicationFactory<T>,若直接在測試中使用 TestServer 則沒有這種限制。
WebApplicationFactory<T> 的 T 并不是 TStartup,而是應用程序入口所在的程序集中的任意類型。
娓娓道來
如果我們使用 dotnet 命令行創建一個 ASP.NET Core MVC/WebAPI 的工程。那么它的啟動代碼大概是這樣的:
public?static?class?Program{
????public?static?void?Main(string[]?args)
????{
????????CreateWebHostBuilder(args).Build().Run();
????}
????public?static?IWebHostBuilder?CreateWebHostBuilder(string[]?args)
????{
????????//?Modified?a?little?bit?for?the?sake?of?illustration
????????return?new?WebHostBuilder()
????????????.UseKestrel()
????????????.ConfigureLogging(lb?=>
????????????{
????????????????lb.SetMinimumLevel(LogLevel.Debug).AddConsole();
????????????})
????????????.UseStartup<Startup>();
????}
}
有沒有小伙伴好奇,為什么需要一個 CreateWebHostBuilder 方法?從直觀上看,它是創建并完成基本的 IWebHostBuilder 配置的方法。這個方法應在測試中進行復用以確保測試和應用程序中的 IWebHostBuilder 配置幾乎相同,例如:
[]public?async?Task?should_get_response_text()
{
????IWebHostBuilder?webHostBuilder?=?Program.CreateWebHostBuilder(Array.Empty<string>());
????using?(var?testServer?=?new?TestServer(webHostBuilder))
????using?(HttpClient?client?=?testServer.CreateClient())
????{
????????HttpResponseMessage?response?=?await?client.GetAsync("/message");
????????Assert.Equal(HttpStatusCode.OK,?response.StatusCode);
????????Assert.Equal("Hello",?await?response.Content.ReadAsStringAsync());
????}
}
這個測試是可以順利通過的。但是我們認為將 Program.CreateWebHostBuilder 暴露并不是一個好的感覺。我們更希望把這個配置過程分離。例如分離到一個類中:
public?class?WebHostBuilderConfigurator{
????public?IWebHostBuilder?Configure(IWebHostBuilder?webHostBuilder)
????{
????????return?webHostBuilder
????????????.UseKestrel()
????????????.ConfigureLogging(lb?=>
????????????{
????????????????lb.SetMinimumLevel(LogLevel.Debug).AddConsole();
????????????})
????????????.UseStartup<Startup>();
????}
}
這樣,Program 僅僅包含整個應用程序的入口,CreateWebHostBuilder 方法就被刪掉了:
public?static?void?Main(string[]?args){
????var?webHostBuilder?=?new?WebHostBuilder();
????new?WebHostBuilderConfigurator().Configure(webHostBuilder).Build().Run();
}
測試也就變成了:
[]public?async?Task?should_get_response_text()
{
????IWebHostBuilder?webHostBuilder?=?new?WebHostBuilderConfigurator().Configure(new?WebHostBuilder());
????using?(var?testServer?=?new?TestServer(webHostBuilder))
????using?(HttpClient?client?=?testServer.CreateClient())
????{
????????HttpResponseMessage?response?=?await?client.GetAsync("/message");
????????Assert.Equal(HttpStatusCode.OK,?response.StatusCode);
????????Assert.Equal("Hello",?await?response.Content.ReadAsStringAsync());
????}
}
看起來不錯,測試也通過了真是可喜可賀?,F在我們準備使用更加完善的 WebApplicationFactory<T> 代替 TestServer 進行測試:
[]public?async?Task?should_get_response_text_using_web_app_factory()
{
????using?(var?factory?=?new?WebApplicationFactory<Startup>().WithWebHostBuilder(
????????wb?=>?new?WebHostBuilderConfigurator().Configure(wb)))
????using?(HttpClient?client?=?factory.CreateClient())
????{
????????HttpResponseMessage?response?=?await?client.GetAsync("/message");
????????Assert.Equal(HttpStatusCode.OK,?response.StatusCode);
????????Assert.Equal("Hello",?await?response.Content.ReadAsStringAsync());
????}
}
看起來不錯,但是發現測試運行的時候卻失敗了。并伴有詭異的異常信息:
System.InvalidOperationException?:?No?method?'public?static?IWebHostBuilder?CreateWebHostBuilder(string[]?args)'?found?on?'WebApp.Program'.?Alternatively,?WebApplicationFactory`1?can?be?extended?and?'protected?virtual?IWebHostBuilder?CreateWebHostBuilder()'?can?be?overridden?to?provide?your?own?IWebHostBuilder?instance.哦,真神奇,它怎么找到 WebApp.Program 的?我只告訴了它 Startup 而并沒有提供任何 Program 類型的信息啊?而這個時候,如果我們老老實實的恢復 WebApp.Program 類中的 CreateWebHostBuilder 方法,那么測試就順利通過了。
這是為什么呢?原來讓測試環境盡可能的 Match 執行環境是我們共同的心愿,WebApplicationFactory 希望能夠自動的幫我們解決這個問題,于是它做了如下的假定:
Startup 所在的程序集應當就是應用程序入口(Main)所在的程序集;
應用程序入口所在的類(Program),里面會包含整個創建和配置 IWebHostBuilder 的過程;
創建和配置 IWebHostBuilder 的過程是由應用程序入口所在類的 CreateWebHostBuilder 方法完成的。
只要符合這三個假定,那么你盡可不費吹灰之力就達到了產品測試配置一致的目的。而如果不符合這個假定將讓測試在默認狀態下執行失敗。具體的代碼請參考 這里 和 這里。從 WebHostFactoryResolver 里面可以看出,除了 CreateWebHostBuilder 方法之外,BuildWebHost 也是一個 Convension,只不過主要是為了向前兼容的目的。
在真實的項目中,很可能是不滿足這三個條件的,那么怎么辦呢?還好我們可以通過集成 WebApplicationFactory<T> 并重寫 CreateWebHostBuilder 方法來解決這個問題:
public?class?MyWebApplicationFactory?:?WebApplicationFactory<Startup>{
????protected?override?IWebHostBuilder?CreateWebHostBuilder()
????{
????????var?webHostBuilder?=?new?WebHostBuilder();
????????new?WebHostBuilderConfigurator().Configure(webHostBuilder);
????????return?webHostBuilder;
????}
}
并相應的將測試更改為:
[]public?async?Task?should_get_response_text_using_web_app_factory()
{
????using?(var?factory?=?new?MyWebApplicationFactory())
????using?(HttpClient?client?=?factory.CreateClient())
????{
????????HttpResponseMessage?response?=?await?client.GetAsync("/message");
????????Assert.Equal(HttpStatusCode.OK,?response.StatusCode);
????????Assert.Equal("Hello",?await?response.Content.ReadAsStringAsync());
????}
}
就可以了。
最后,需要提醒的是 WebApplicationFactory<T> 的 T 是 TEntryPoint ,是入口所在的程序集的類型。雖然平常大家都喜歡寫 Startup。
總結
請飛到文章開頭~ :-D
總結
以上是生活随笔為你收集整理的【ASP.NET Core 沉思录】CreateWebHostBuilder 是一个 Convension的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 东莞.NET技术线下沙龙活动资料分享
- 下一篇: .Netcore 2.0 Ocelot