Asp.Net Core利用xUnit進(jìn)行主機(jī)級(jí)別的網(wǎng)絡(luò)集成測(cè)試詳解
前言
在開(kāi)發(fā) Asp.Net Core 應(yīng)用程序的過(guò)程中,我們常常需要對(duì)業(yè)務(wù)代碼編寫單元測(cè)試,這種方法既快速又有效,利用單元測(cè)試做代碼覆蓋測(cè)試,也是非常必要的事情;但是,但我們需要對(duì)系統(tǒng)進(jìn)行集成測(cè)試的時(shí)候,需要啟動(dòng)服務(wù)主機(jī),利用瀏覽器或者Postman 等網(wǎng)絡(luò)工具對(duì)接口進(jìn)行集成測(cè)試,這就非常的不方便,同時(shí)浪費(fèi)了大量的時(shí)間在重復(fù)啟動(dòng)應(yīng)用程序上;今天要介紹就是如何在不啟動(dòng)應(yīng)用程序的情況下,對(duì) Asp.Net Core WebApi 項(xiàng)目進(jìn)行網(wǎng)絡(luò)集成測(cè)試。
一、建立項(xiàng)目
1.1 首先我們建立兩個(gè)項(xiàng)目,Asp.Net Core WebApi 和 xUnit 單元測(cè)試項(xiàng)目,如下
1.2 上圖的單元測(cè)試項(xiàng)目 Ron.XUnitTest 必須應(yīng)用待測(cè)試的 WebApi 項(xiàng)目 Ron.TestDemo
1.3 接下來(lái)打開(kāi) Ron.XUnitTest 項(xiàng)目文件 .csproj,添加包引用
Microsoft.AspNetCore.App Microsoft.AspNetCore.TestHost
1.4 為什么要引用這兩個(gè)包呢,因?yàn)槲覄偛艅?chuàng)建的 WebApi 項(xiàng)目是引用 Microsoft.AspNetCore.App 的,至于 Microsoft.AspNetCore.TestHost,它是今天的主角,為了使用測(cè)試主機(jī),必須對(duì)其進(jìn)行引用,下面會(huì)詳細(xì)說(shuō)明
二、編寫業(yè)務(wù)
2.1 創(chuàng)建一個(gè)接口,代碼如下
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private IConfiguration configuration; public ValuesController(IConfiguration configuration) { this.configuration = configuration; } [HttpGet("{id}")] public ActionResult<int> Get(int id) { var result= id + this.configuration.GetValue<int>("max"); return result; } }
2.1 接口代碼非常簡(jiǎn)單,接受一個(gè)參數(shù) id,然后和配置文件中獲取的值 max 相加,然后輸出結(jié)果給客戶端
三、編寫測(cè)試用例
3.1 為了能夠使用主機(jī)集成測(cè)試,我們需要使用類
Microsoft.AspNetCore.TestHost.TestServer
3.2 我們來(lái)看一下 TestServer 的源碼,代碼較長(zhǎng),你可以直接跳過(guò)此段,進(jìn)入下一節(jié) 3.3
public class TestServer : IServer { private IWebHost _hostInstance; private bool _disposed = false; private IHttpApplication<Context> _application; public TestServer(): this(new FeatureCollection()) { } public TestServer(IFeatureCollection featureCollection) { Features = featureCollection ?? throw new ArgumentNullException(nameof(featureCollection)); } public TestServer(IWebHostBuilder builder): this(builder, new FeatureCollection()) { } public TestServer(IWebHostBuilder builder, IFeatureCollection featureCollection): this(featureCollection) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } var host = builder.UseServer(this).Build(); host.StartAsync().GetAwaiter().GetResult(); _hostInstance = host; } public Uri BaseAddress { get; set; } = new Uri("http://localhost/"); public IWebHost Host { get { return _hostInstance ?? throw new InvalidOperationException("The TestServer constructor was not called with a IWebHostBuilder so IWebHost is not available."); } } public IFeatureCollection Features { get; } private IHttpApplication<Context> Application { get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured."); } public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); return new ClientHandler(pathBase, Application); } public HttpClient CreateClient() { return new HttpClient(CreateHandler()) { BaseAddress = BaseAddress }; } public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); return new WebSocketClient(pathBase, Application); } public RequestBuilder CreateRequest(string path) { return new RequestBuilder(this, path); } public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, CancellationToken cancellationToken = default) { if (configureContext == null) { throw new ArgumentNullException(nameof(configureContext)); } var builder = new HttpContextBuilder(Application); builder.Configure(context => { var request = context.Request; request.Scheme = BaseAddress.Scheme; request.Host = HostString.FromUriComponent(BaseAddress); if (BaseAddress.IsDefaultPort) { request.Host = new HostString(request.Host.Host); } var pathBase = PathString.FromUriComponent(BaseAddress); if (pathBase.HasValue && pathBase.Value.EndsWith("/")) { pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1)); } request.PathBase = pathBase; }); builder.Configure(configureContext); return await builder.SendAsync(cancellationToken).ConfigureAwait(false); } public void Dispose() { if (!_disposed) { _disposed = true; _hostInstance.Dispose(); } } Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) { _application = new ApplicationWrapper<Context>((IHttpApplication<Context>)application, () => { if (_disposed) { throw new ObjectDisposedException(GetType().FullName); } }); return Task.CompletedTask; } Task IServer.StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } private class ApplicationWrapper<TContext> : IHttpApplication<TContext> { private readonly IHttpApplication<TContext> _application; private readonly Action _preProcessRequestAsync; public ApplicationWrapper(IHttpApplication<TContext> application, Action preProcessRequestAsync) { _application = application; _preProcessRequestAsync = preProcessRequestAsync; } public TContext CreateContext(IFeatureCollection contextFeatures) { return _application.CreateContext(contextFeatures); } public void DisposeContext(TContext context, Exception exception) { _application.DisposeContext(context, exception); } public Task ProcessRequestAsync(TContext context) { _preProcessRequestAsync(); return _application.ProcessRequestAsync(context); } } }
3.3 TestServer 類代碼量比較大,不過(guò)不要緊,我們只需要關(guān)注它的構(gòu)造方法就可以了
public TestServer(IWebHostBuilder builder) : this(builder, new FeatureCollection()) { }
3.4 其構(gòu)造方法接受一個(gè) IWebHostBuilder 對(duì)象,只要我們傳入一個(gè) WebHostBuilder 就可以創(chuàng)建一個(gè)測(cè)試主機(jī)了
3.5 創(chuàng)建測(cè)試主機(jī)和 HttpClient 客戶端,我們?cè)跍y(cè)試類 ValuesUnitTest 編寫如下代碼
public class ValuesUnitTest { private TestServer testServer; private HttpClient httpCLient; public ValuesUnitTest() { testServer = new TestServer(new WebHostBuilder().UseStartup<Ron.TestDemo.Startup>()); httpCLient = testServer.CreateClient(); } [Fact] public async void GetTest() { var data = await httpCLient.GetAsync("/api/values/100"); var result = await data.Content.ReadAsStringAsync(); Assert.Equal("300", result); } }
代碼解釋
這段代碼非常簡(jiǎn)單,首先,我們聲明了一個(gè) TestServer 和 HttpClient 對(duì)象,并在構(gòu)造方法中初始化他們; TestServer 的初始化是由我們 new 了一個(gè) Builder 對(duì)象,并指定其使用待測(cè)試項(xiàng)目 Ron.TestDemo 中的 Startup 類來(lái)啟動(dòng),這樣我們能可以直接使用待測(cè)試項(xiàng)目的路由和管道了,甚至我們無(wú)需指定測(cè)試站點(diǎn),因?yàn)檫@些都會(huì)在 TestServer 自動(dòng)配置一個(gè) localhost 的主機(jī)地址
3.7 接下來(lái)就是創(chuàng)建了一個(gè)單元測(cè)試的方法,直接使用剛才初始化的 HttpClient 對(duì)象進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這個(gè)時(shí)候,我們只需要知道 Action 即可,同時(shí)傳遞參數(shù) 100,最后斷言服務(wù)器輸出值為:"300",回顧一下我們創(chuàng)建的待測(cè)試方法,其業(yè)務(wù)正是將客戶端傳入的 id 值和配置文件 max 值相加后輸出,而 max 值在這里被配置為 200
3.8 運(yùn)行單元測(cè)試
3.9 測(cè)試通過(guò),可以看到,測(cè)試達(dá)到了預(yù)期的結(jié)果,服務(wù)器正確返回了計(jì)算后的值
四、配置文件注意事項(xiàng)
4.1 在待測(cè)試項(xiàng)目中的配置文件 appsettings.json 并不會(huì)被測(cè)試主機(jī)所讀取,因?yàn)槲覀冊(cè)谏厦鎰?chuàng)建測(cè)試主機(jī)的時(shí)候沒(méi)有調(diào)用方法
WebHost.CreateDefaultBuilder
4.2 我們只是創(chuàng)建了一個(gè) WebHostBuilder 對(duì)象,非常輕量的主機(jī)配置,簡(jiǎn)單來(lái)說(shuō)就是無(wú)配置,如果對(duì)于 WebHost.CreateDefaultBuilder 不理解的同學(xué),建議閱讀我的文章 asp.netcore 深入了解配置文件加載過(guò)程.
4.3 所以,為了能夠在單元測(cè)試中使用項(xiàng)目配置文件,我在 Ron.TestDemo 項(xiàng)目中的 Startup 類加入了下面的代碼
public class Startup { public Startup(IConfiguration configuration, IHostingEnvironment env) { this.Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .SetBasePath(env.ContentRootPath) .Build(); } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfiguration>(this.Configuration); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } }
4.4 其目的就是手動(dòng)讀取配置文件,重新初始化 IConfiguration 對(duì)象,并將 this.Configuration 對(duì)象加入依賴注入容器中
結(jié)語(yǔ)
- 本文從單元測(cè)試入手,針對(duì)常見(jiàn)的系統(tǒng)集成測(cè)試提供了另外一種便捷的測(cè)試方案,通過(guò)創(chuàng)建 TestServer 測(cè)試主機(jī)開(kāi)始,利用主機(jī)創(chuàng)建 HttpCLient 對(duì)象進(jìn)行網(wǎng)絡(luò)集成測(cè)試
- 減少重復(fù)啟動(dòng)程序和測(cè)試工具,提高了測(cè)試效率
- 充分利用了 Visual Studio 的優(yōu)勢(shì),既可以做單元測(cè)試,還能利用這種測(cè)試方案進(jìn)行快速代碼調(diào)試
- 最后,還了解如何通過(guò) TestServer 主機(jī)加載待測(cè)試項(xiàng)目的配置文件對(duì)象 IConfiguration
示例代碼下載
http://xiazai.jb51.net/201812/yuanma/Ron.TestDemo_jb51.rar
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
使用HttpClient增刪改查ASP.NET Web API服務(wù)
這篇文章介紹了使用HttpClient增刪改查ASP.NET Web API服務(wù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-10-10.Net基于Thread實(shí)現(xiàn)自旋鎖的三種方式
本文主要講解.Net基于Thread實(shí)現(xiàn)自旋鎖的三種方式,基于Test--And--Set原子操作實(shí)現(xiàn),包含優(yōu)缺點(diǎn)介紹,感興趣的朋友跟隨小編一起看看吧2021-06-06如何利用HttpClientFactory實(shí)現(xiàn)簡(jiǎn)單的熔斷降級(jí)
這篇文章主要給大家介紹了關(guān)于如何利用HttpClientFactory實(shí)現(xiàn)簡(jiǎn)單的熔斷降級(jí)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07C#中使用SQLite數(shù)據(jù)庫(kù)的方法介紹
SQLite是一個(gè)開(kāi)源的輕量級(jí)的桌面型數(shù)據(jù)庫(kù),它將幾乎所有數(shù)據(jù)庫(kù)要素(包括定義、表、索引和數(shù)據(jù)本身)都保存在一個(gè)單一的文件中。SQLite用C編寫實(shí)現(xiàn),它在內(nèi)存消耗、文件體積、操作性能、簡(jiǎn)單性方面都有不錯(cuò)的表現(xiàn)2012-01-01ASP.NET Global.asax應(yīng)用程序文件簡(jiǎn)介
Global.asax 文件,有時(shí)候叫做 ASP.NET 應(yīng)用程序文件,提供了一種在一個(gè)中心位置響應(yīng)應(yīng)用程序級(jí)或模塊級(jí)事件的方法。2009-03-03asp.net(c#) RSS功能實(shí)現(xiàn)代碼
這兩天一邊從網(wǎng)上找資料,自己再測(cè)試,終于完成本站的RSS功能了!先自我恭喜下!!2008-11-11ASP.NET 4.0配置文件中的ClientIDMode屬性詳解
在ASP.NET 4.0中的每個(gè)控件上都多了一個(gè)叫做ClientIDMode的屬性,本文主要介紹了ASP.NET 4.0配置文件中的ClientIDMode屬性詳解,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-11-11asp.net coolite 刪除時(shí)彈出確定按鈕
如果用coolite的 Confirm() 是不知道你選擇了什么的 如上代碼才可以的2009-09-09asp.net mvc CodeFirst模式數(shù)據(jù)庫(kù)遷移步驟詳解
這篇文章主要為大家詳細(xì)介紹了asp.net mvc CodeFirst模式數(shù)據(jù)庫(kù)遷移步驟,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10