热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Asp.NetCore轻松学-利用xUnit进行主机级别的网络集成测试

前言    在开发Asp.NetCore应用程序的过程中,我们常常需要对业务代码编写单元测试,这种方法既快速又有效,利用单元测试做代码覆盖测试,也是非常必要的事情;但是,但我们需要对系统进行

前言

    在开发 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 Get(int id)
        {
            var result= id + this.configuration.GetValue("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 _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 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 SendAsync(Action 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(IHttpApplication application, CancellationToken cancellationToken)
        {
            _application = new ApplicationWrapper((IHttpApplication)application, () =>
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException(GetType().FullName);
                }
            });

            return Task.CompletedTask;
        }

        Task IServer.StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }

        private class ApplicationWrapper : IHttpApplication
        {
            private readonly IHttpApplication _application;
            private readonly Action _preProcessRequestAsync;

            public ApplicationWrapper(IHttpApplication 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());
            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(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


推荐阅读
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • Android 自定义 RecycleView 左滑上下分层示例代码
    为了满足项目需求,需要在多个场景中实现左滑删除功能,并且后续可能在列表项中增加其他功能。虽然网络上有很多左滑删除的示例,但大多数封装不够完善。因此,我们尝试自己封装一个更加灵活和通用的解决方案。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • javax.mail.search.BodyTerm.matchPart()方法的使用及代码示例 ... [详细]
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • com.sun.javadoc.PackageDoc.exceptions()方法的使用及代码示例 ... [详细]
  • 本文介绍如何在 Android 中自定义加载对话框 CustomProgressDialog,包括自定义 View 类和 XML 布局文件的详细步骤。 ... [详细]
  • 原文网址:https:www.cnblogs.comysoceanp7476379.html目录1、AOP什么?2、需求3、解决办法1:使用静态代理4 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 本文详细解析了Autofac在高级应用场景中的具体实现,特别是如何通过注册泛型接口的类来优化依赖注入。示例代码展示了如何使用 `builder.RegisterAssemblyTypes` 方法,结合 `typeof(IEventHandler).Assembly` 和 `Where` 过滤条件,动态注册所有符合条件的类,从而简化配置并提高代码的可维护性。此外,文章还探讨了这一方法在复杂系统中的实际应用及其优势。 ... [详细]
author-avatar
jing阿囡宝_478
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有