前言

    在开发 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,添加包引用

Microsoft.AspNetCore.App
Microsoft.AspNetCore.TestHost

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.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. 配置文件注意事项

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

WebHost.CreateDefaultBuilder

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

4.3 所以,为了能够在单元测试中使用项目配置文件,我在 Ron.TestDemo 项目中的 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 其目的就是手动读取配置文件,重新初始化 IConfiguration 对象,并将 this.Configuration 对象加入依赖注入容器中

结语

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

示例代码下载

https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.TestDemo

Asp.Net Core 轻松学-利用xUnit进行主机级别的网络集成测试的更多相关文章

  1. Asp.Net Core 轻松学-利用文件监视进行快速测试开发

    前言     在进行 Asp.Net Core 应用程序开发过程中,通常的做法是先把业务代码开发完成,然后建立单元测试,最后进入本地系统集成测试:在这个过程中,程序员的大部分时间几乎都花费在开发.运行 ...

  2. Asp.Net Core 轻松学-利用 Swagger 自动生成接口文档

    前言     目前市场上主流的开发模式,几乎清一色的前后端分离方式,作为服务端开发人员,我们有义务提供给各个客户端良好的开发文档,以方便对接,减少沟通时间,提高开发效率:对于开发人员来说,编写接口文档 ...

  3. Asp.Net Core 轻松学-利用日志监视进行服务遥测

    前言     在 Net Core 2.2 中,官方文档表示,对 EventListener 这个日志监视类的内容进行了扩充,同时赋予了跟踪 CoreCLR 事件的权限:通过跟踪 CoreCLR 事件 ...

  4. Asp.Net Core 轻松学系列-1阅读指引目录

    https://www.cnblogs.com/viter/p/10474091.html 目录 前言 1. 从安装到配置 2. 业务实现 3. 日志 4. 测试 5. 缓存使用 6.网络和通讯 7. ...

  5. Asp.Net Core 轻松学-使用MariaDB/MySql/PostgreSQL和支持多个上下文对象

    前言 在上一篇文章中(Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库)[https://www.cnblogs.com/viter/p/10243577.html],介 ...

  6. 如何从40亿整数中找到不存在的一个 webservice Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库 WPF实战案例-打印 RabbitMQ与.net core(五) topic类型 与 headers类型 的Exchange

    如何从40亿整数中找到不存在的一个 前言 给定一个最多包含40亿个随机排列的32位的顺序整数的顺序文件,找出一个不在文件中的32位整数.(在文件中至少确实一个这样的数-为什么?).在具有足够内存的情况 ...

  7. WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)

    WebAPI调用笔记   前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...

  8. C# 中一些类关系的判定方法 C#中关于增强类功能的几种方式 Asp.Net Core 轻松学-多线程之取消令牌

    1.  IsAssignableFrom实例方法 判断一个类或者接口是否继承自另一个指定的类或者接口. public interface IAnimal { } public interface ID ...

  9. Asp.Net Core 轻松学-多线程之Task(补充)

    前言     在上一章 Asp.Net Core 轻松学-多线程之Task快速上手 文章中,介绍了使用Task的各种常用场景,但是感觉有部分内容还没有完善,在这里补充一下. 1. 任务的等待 在使用 ...

随机推荐

  1. BZOJ_4892_[Tjoi2017]dna_哈希

    BZOJ_4892_[Tjoi2017]dna_哈希 Description 加里敦大学的生物研究所,发现了决定人喜不喜欢吃藕的基因序列S,有这个序列的碱基序列就会表现出喜欢吃藕的 性状,但是研究人员 ...

  2. javaWeb使用POI操作Excel

    1.为项目添加POI POI官网链接 点进去之后下载(上边的是编译好的类,下边的是源代码) 解压文件夹,把下面三个文件复制到WebComtent>WEB-INF>lib文件夹下 再把这三个 ...

  3. nginx用户认证与htpasswd命令

    最近在搭建ELK,然后ELK的kibana界面想添加一个访问限制,看到kibana有个插件x-pack,本来想用用,发现是收费的,就放弃了,然后就想着想配置下nginx的认证访问来实现简单的访问登陆. ...

  4. js继承之组合继承(结合原型链继承 和 借用构造函数继承)

    在我的前两篇文章中,我们已经介绍了 js 中实现继承的两种模式:原型链继承和借用构造函数继承.这两种模式都存在各自的缺点,所以,我们考虑是否能将这二者结合到一起,从而发挥二者之长.即在继承过程中,既可 ...

  5. c#位运算基本概念与计算过程

    前言 一些非常基础的东西,在实际工作中没有用到.很少用到.一旦遇到,又不知所云.最近遇到一个问题,把一个int16(short) .两个bool变量整合成一个int32(int),当听到这个要求时,我 ...

  6. 从壹开始 [ Nuxt.js ] 之二 || 项目搭建 与 接口API

    前言 哈喽大家周一好,今天的内容比较多,主要就是包括:把前端页面的展示页给搭出来,然后调通接口API,可以添加数据,这两天我也一直在开发,本来想一篇一篇的写,发现可能会比较简单,就索性把项目搭建的过程 ...

  7. Mysql常用基础操作(备忘录)

    常常忘记mysql的一些命令行操作,甚至于说,比较复杂的sql格式记不住或忘记了,也可能根本不会考虑去记,因此,做一下汇总,当下次出现恍惚时不至于去百度挨个找,有时就是记不起来,但是只要给点药引子,立 ...

  8. java~springboot~目录索引

    回到占占推荐博客索引 最近写了不过关于java,spring,微服务的相关文章,今天把它整理一下,方便大家学习与参考. java~springboot~目录索引 Java~关于开发工具和包包 Java ...

  9. 【4】Asp.Net Core2.2中间件多扩展对应应用

    [前言] 上一篇完成了Asp.Net Core 2.2全新的管道处理模型解析,“俄罗斯套娃”式的委托嵌套和传递,组建了扩展性无与伦比的管道模型!与此同时,委托嵌套过于复杂,使用起来并不友好,然后多种扩 ...

  10. 使用JDBC操作MySQL数据库

    一.JDBC简介 JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一的访问,它由一组用Java语言编写的类和接口组 ...