Asp.Net Core利用xUnit进行主机级别的网络集成测试详

编程学习 2021-07-04 22:41www.dzhlxh.cn编程入门
这篇文章主要给大家介绍了关于Asp.Net Core利用xUnit进行主机级别的网络集成测试的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们来一起看看吧

前言

在开发 Asp.Net Core 应用程序的过程中,我们常常需要对业务代码编写单元测试,这种方法既快速又有效,利用单元测试做代码覆盖测试,也是非常必要的事情;但是,但我们需要对系统进行集成测试的时候,需要启动服务主机,利用浏览器或者Postman 等网络工具对接口进行集成测试,这就非常的不方便,同时浪费了大量的时间在重复启动应用程序上;今天要介绍就是如何在不启动应用程序的情况下,对 Asp.Net Core WebApi 项目进行网络集成测试。

一、建立项目

1.1 首先我们建立两个项目,Asp.Net Core WebApi 和 xUnit 单元测试项目,如下

1.2 上图的单元测试项目 Ron.XUnitTest 必须应用待测试的 WebApi 项目 Ron.TestDemo

1.3 接下来打开 Ron.XUnitTest 项目文件 .csproj,添加包引用

Microsoft.AspNetCore.App
Microsoft.AspNetCore.TestHost

1.4 为什么要引用这两个包呢,因为我刚才创建的 WebApi 项目是引用 Microsoft.AspNetCore.App 的,至于 Microsoft.AspNetCore.TestHost,它是今天的主角,为了使用测试主机,必须对其进行引用,狼蚁网站SEO优化会详细说明

二、编写业务

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.1 为了能够使用主机集成测试,我们需要使用类

Microsoft.AspNetCore.TestHost.TestServer

3.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("/"))
  {
   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 类代码量比较大,不过不要紧,我们只需要关注它的构造方法就可以了

 public TestServer(IWebHostBuilder builder)
  : this(builder, new FeatureCollection())
 {
 }

3.4 其构造方法接受一个 IWebHostBuilder 对象,只要我们传入一个 WebHostBuilder 就可以创建一个测试主机了

3.5 创建测试主机和 HttpClient 客户端,我们在测试类 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);
 }
 }

代码解释

这段代码非常简单,首先,我们声明了一个 TestServer 和 HttpClient 对象,并在构造方法中初始化他们; TestServer 的初始化是由我们 new 了一个 Builder 对象,并指定其使用待测试项目 Ron.TestDemo 中的 Startup 类来启动,这样我们能可以直接使用待测试项目的路由和管道了,甚至我们无需指定测试站点,因为这些都会在 TestServer 自动配置一个 localhost 的主机地址

3.7 接下来就是创建了一个单元测试的方法,直接使用刚才初始化的 HttpClient 对象进行网络请求,这个时候,我们只需要知道 Action 即可,同时传递参数 100,最后断言服务器输出值为:"300",回顾一下我们创建的待测试方法,其业务正是将客户端传入的 id 值和配置文件 max 值相加后输出,而 max 值在这里被配置为 200

3.8 运行单元测试

3.9 测试通过,可以看到,测试达到了预期的结果,服务器正确返回了计算后的值

四、配置文件注意事项

4.1 在待测试项目中的配置文件 appsettings.json 并不会被测试主机所读取,因为我们在上面创建测试主机的时候没有调用方法

WebHost.CreateDefaultBuilder

4.2 我们只是创建了一个 WebHostBuilder 对象,非常轻量的主机配置,简单来说就是无配置,如果对于 WebHost.CreateDefaultBuilder 不理解的同学,建议阅读我的文章 .

4.3 所以,为了能够在单元测试中使用项目配置文件,我在 Ron.TestDemo 项目中的 Startup 类加入了狼蚁网站SEO优化的代码

 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 其目的就是手动读取配置文件,重新初始化 IConfiguration 对象,并将 this.Configuration 对象加入依赖注入容器中

结语

  • 本文从单元测试入手,针对常见的系统集成测试提供了另外一种便捷的测试方案,通过创建 TestServer 测试主机开始,利用主机创建 HttpCLient 对象进行网络集成测试
  • 减少重复启动程序和测试工具,提高了测试效率
  • 充分利用了 Visual Studio 的优势,既可以做单元测试,还能利用这种测试方案进行快速代码调试
  • 最后,还了解如何通过 TestServer 主机加载待测试项目的配置文件对象 IConfiguration

示例代码下载

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对狼蚁SEO的支持。

Copyright © 2016-2025 www.dzhlxh.cn 金源码 版权所有 Power by

网站模板下载|网络推广|微博营销|seo优化|视频营销|网络营销|微信营销|网站建设|织梦模板|小程序模板