在开发 Asp.Net Core 应用程序的过程中,我们常常需要对业务代码编写单元测试,这种方法既快速又有效,利用单元测试做代码覆盖测试,也是非常必要的事情;但是,但我们需要对系统进行集成测试的时候,需要启动服务主机,利用浏览器或者Postman 等网络工具对接口进行集成测试,这就非常的不方便,同时浪费了大量的时间在重复启动应用程序上;今天要介绍就是如何在不启动应用程序的情况下,对 Asp.Net Core WebApi 项目进行网络集成测试。
1.1 建立项目1.1 首先我们建立两个项目,Asp.Net Core WebApi 和 xUnit 单元测试项目,如下
1.2 上图的单元测试项目 Ron.XUnitTest 必须应用待测试的 WebApi 项目 Ron.TestDemo
1.3 接下来打开 Ron.XUnitTest 项目文件 .csproj,添加包引用
1.4 为什么要引用这两个包呢,因为我刚才创建的 WebApi 项目是引用 Microsoft.AspNetCore.App 的,至于 Microsoft.AspNetCore.TestHost,它是今天的主角,为了使用测试主机,必须对其进行引用,下面会详细说明
2. 编写业务2.1 创建一个接口,代码如下
[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 接口代码非常简单,接受一个参数 id,然后和配置文件中获取的值 max 相加,然后输出结果给客户端
3. 编写测试用例3.1 为了能够使用主机集成测试,我们需要使用类
Microsoft.AspNetCore.TestHost.TestServer3.2 我们来看一下 TestServer 的源码,代码较长,你可以直接跳过此段,进入下一节 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("http://www.likecs.com/")) { 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); } } }