Asp.Net Core利用xUnit進(jìn)行主機(jī)級(jí)別的網(wǎng)絡(luò)集成測(cè)試詳解
前言
在開(kāi)發(fā) Asp.Net Core 應(yīng)用程序的過(guò)程中,我們常常需要對(duì)業(yè)務(wù)代碼編寫(xiě)單元測(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ō)明
二、編寫(xiě)業(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é)果給客戶(hù)端
三、編寫(xiě)測(cè)試用例
3.1 為了能夠使用主機(jī)集成測(cè)試,我們需要使用類(lèi)
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 類(lèi)代碼量比較大,不過(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 客戶(hù)端,我們?cè)跍y(cè)試類(lèi) ValuesUnitTest 編寫(xiě)如下代碼
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)來(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ù)正是將客戶(hù)端傳入的 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 類(lèi)加入了下面的代碼
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ì)象加入依賴(lài)注入容器中
結(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-07
C#中使用SQLite數(shù)據(jù)庫(kù)的方法介紹
SQLite是一個(gè)開(kāi)源的輕量級(jí)的桌面型數(shù)據(jù)庫(kù),它將幾乎所有數(shù)據(jù)庫(kù)要素(包括定義、表、索引和數(shù)據(jù)本身)都保存在一個(gè)單一的文件中。SQLite用C編寫(xiě)實(shí)現(xiàn),它在內(nèi)存消耗、文件體積、操作性能、簡(jiǎn)單性方面都有不錯(cuò)的表現(xiàn)2012-01-01
ASP.NET Global.asax應(yīng)用程序文件簡(jiǎn)介
Global.asax 文件,有時(shí)候叫做 ASP.NET 應(yīng)用程序文件,提供了一種在一個(gè)中心位置響應(yīng)應(yīng)用程序級(jí)或模塊級(jí)事件的方法。2009-03-03
asp.net(c#) RSS功能實(shí)現(xiàn)代碼
這兩天一邊從網(wǎng)上找資料,自己再測(cè)試,終于完成本站的RSS功能了!先自我恭喜下!!2008-11-11
ASP.NET 4.0配置文件中的ClientIDMode屬性詳解
在ASP.NET 4.0中的每個(gè)控件上都多了一個(gè)叫做ClientIDMode的屬性,本文主要介紹了ASP.NET 4.0配置文件中的ClientIDMode屬性詳解,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-11-11
asp.net coolite 刪除時(shí)彈出確定按鈕
如果用coolite的 Confirm() 是不知道你選擇了什么的 如上代碼才可以的2009-09-09
asp.net mvc CodeFirst模式數(shù)據(jù)庫(kù)遷移步驟詳解
這篇文章主要為大家詳細(xì)介紹了asp.net mvc CodeFirst模式數(shù)據(jù)庫(kù)遷移步驟,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10

