zoukankan      html  css  js  c++  java
  • [Abp vNext 源码分析]

    简介

    ABP vNext 框架使用 xUnit 作为单元测试组件,官方的所有模块都编写了大量的 单元/集成测试 确保功能正常。由于 ABP vNext 模块化系统的原因,开发人员在建立单元测试项目的时候需要集成 Volo.Abp.UnitTest 项目,这样在执行单元测试的时候才不会缺少必要组件。

    分析

    ABP vNext 单元测试相关的类型最核心的是集成测试基类 AbpIntegratedTest 和 MVC 专用测试基类 AbpAspNetCoreIntegratedTestBase,这两个基类核心工作就是初始化 IoC 容器并且初始化整个模块系统,只不过后者对 控制器 相关的组件进行了初始化配置,让开发人员可以针对 控制器 进行单元/集成测试。

    从上图可以看到两个基类都继承自 AbpTestBaseWithServiceProvider 基类,在这个基类里面将 IServiceProvider 作为一个抽象成员。这是因为 MVC 和测试基类的 ServiceProvider 来源不一样,一个是 ABP vNext 根据 Application 类已注册 IoC 容器构建的,另一个使用的是 IHost 测试主机内的 ServiceProvider

    单元测试执行本质上就是将测试类进行实例化,然后调用对应的单元测试方法,所以测试基类的初始化动作都是放在对应的无参构造函数。

    虽然 Volo.Abp.UnitTest 也是单独的一个项目,它的 AbpTestBaseModule 是没有任何动作,仅仅是为了同其他项目保持一致,内部是没有任何代码。

    using Volo.Abp.Modularity;
    
    namespace Volo.Abp
    {
        public class AbpTestBaseModule : AbpModule
        {
    
        }
    }
    
    

    集成测试基类

    一般来说,我们会直接从 AbpIntegratedTest 继承并实现我们需要的单元测试基类,包括 ABP vNext 官方的默认模版也是这样。集成测试基类的核心代码很简单,就是在无参构造函数的内部进行初始化动作,且在过程中按顺序执行两个生命周期方法。

    简易流程图:

    start=>start: 开始单元测试 op1=>operation: BeforeAddApplication() op2=>operation: 注册模块服务 op3=>operation: AfterAddApplication() op4=>operation: 模块初始化 op5=>operation: 执行单元测试 end=>end: 销毁相关资源

    start->op1->op2->op3->op4->op5->end

    public abstract class AbpIntegratedTest<TStartupModule> : AbpTestBaseWithServiceProvider, IDisposable
        where TStartupModule : IAbpModule
    {
        protected IAbpApplication Application { get; }
    
        protected override IServiceProvider ServiceProvider => Application.ServiceProvider;
    
        protected IServiceProvider RootServiceProvider { get; }
    
        protected IServiceScope TestServiceScope { get; }
    
        protected AbpIntegratedTest()
        {
            var services = CreateServiceCollection();
    
            BeforeAddApplication(services);
    
            var application = services.AddApplication<TStartupModule>(SetAbpApplicationCreationOptions);
            Application = application;
    
            AfterAddApplication(services);
    
          	// 根据已有 IServiceCollection 创建 IoC 容器。
            RootServiceProvider = CreateServiceProvider(services);
            TestServiceScope = RootServiceProvider.CreateScope();
    				
          	// 使用子容器对 ABP 模块系统进行初始化。
            application.Initialize(TestServiceScope.ServiceProvider);
        }
    
        // ... 其他代码。
    }
    

    上述代码可以看到默认的测试基类并没有直接使用 RootServiceProvider,而是创建了一个子容器给 ABP vNext 使用,这主要是为了后续可以对容器进行销毁操作,具体可一看下面的 Dispose() 方法。

    public virtual void Dispose()
    {
        Application.Shutdown();
        TestServiceScope.Dispose();
        Application.Dispose();
    }
    

    总的来说,测试基类就是构建了一个 IAbpApplication 对象,根据传入的 TStartupModule 模块进入拓扑排序过程,依次执行各个模块的生命周期配置。

    MVC 测试基类

    针对我们的 Http Api 层,如果需要对 Controller 进行测试的话,就需要从 MVC 测试基类继承编写单元/集成测试,各个类型的关系如下。

    classDiagram class AbpTestBaseWithServiceProvider { #ServiceProvider #GetService() ~T~ #GetRequiredService() ~T~ } class AbpIntegratedTest~TStartupModule~{ #BeforeAddApplication(IServiceCollection service) #SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) #AfterAddApplication(IServiceCollection services) #CreateServiceProvider(IServiceCollection service) +Dispose() } class AbpAspNetCoreIntegratedTestBase~TStartup~{ #TestServer Server #HttpClient Client -IHost host #CreateHostBuilder() IHostBuilder #ConfigureServices(HostBuilderContext context, IServiceCollection services) #GetUrl_OfType_TController() string #GetUrl_OfType_TController(string actionName) string #GetUrl_OfType_TController(string actionName, object queryStringParamsAsAnonymousObject) string +Dispose() } class IDispose{ <<interface>> IDispose } AbpIntegratedTest~TStartupModule~ --|> AbpTestBaseWithServiceProvider AbpIntegratedTest~TStartupModule~ ..|> IDispose AbpAspNetCoreIntegratedTestBase~TStartup~ --|> AbpTestBaseWithServiceProvider AbpAspNetCoreIntegratedTestBase~TStartup~ ..|> IDispose

    针对 AspNetCore 来说,ABP 创建了一个新的 Host 主机,在每次执行测试的时候会启动一个新的 Web 服务器。(并不会创建真实服务,不存在端口占用问题)

    在基类当中,ABP 定义了两个属性 ServerClient,它们都是 Mock 了对应的接口,方便后续的单元测试,这里的 ITestServerAccessor 接口是用于 Mock AspNetCoreTestDynamicProxyHttpClientFactory 接口所需要的。

    AspNetCoreTestDynamicProxyHttpClientFactory 接口是 ABP 底层进行动态代理所使用的,在请求远程服务的时候会调用这个接口创建 HttpClient 对象。

    protected AbpAspNetCoreIntegratedTestBase()
    {
        var builder = CreateHostBuilder();
    
        _host = builder.Build();
        _host.Start();
    
        Server = _host.GetTestServer();
        Client = _host.GetTestClient();
    
        ServiceProvider = Server.Services;
    
        ServiceProvider.GetRequiredService<ITestServerAccessor>().Server = Server;
    }
    

    从 UML 类图当中,可以看到基类定义了几个 GetUrl() 方法,这几个方法是根据 Controller 获取对应的请求路径。

    这里我以一个 SampleController 控制器为例,它提供了一个 Index 方法,返回了一个页面内容。针对它来说,我们编写集成测试是这样操作的。

    public class SimpleController : AbpController
    {
        public ActionResult Index()
        {
            return Content("Index-Result");
        }
    }
    

    集成测试

    public class SimpleController_Tests : AbpAspNetCoreIntegratedTestBase<Startup>
    {
        protected virtual async Task<HttpResponseMessage> GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
        {
            using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
            {
                requestMessage.Headers.Add("Accept-Language", CultureInfo.CurrentUICulture.Name);
                var response = await Client.SendAsync(requestMessage);
                response.StatusCode.ShouldBe(expectedStatusCode);
                return response;
            }
        }
    
        protected virtual async Task<string> GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
        {
            using (var response = await GetResponseAsync(url, expectedStatusCode))
            {
                return await response.Content.ReadAsStringAsync();
            }
        }
    
        [Fact]
        public async Task ActionResult_ContentResult()
        {
            var result = await GetResponseAsStringAsync(
                GetUrl<SimpleController>(nameof(SimpleController.Index))
            );
    
            result.ShouldBe("Index-Result");
        }
    }
    

    EF Core 的集成

    在执行单元测试过程中,我们难免会对数据库进行操作。这个时候不可能连接真实数据库,就需要我们在测试基类当中进行一些初始化动作,将底层的数据库链接改为 SQLite 的内存模式。

    public class SampleEntityFrameworkCoreTestModule : AbpModule
    {
        private SqliteConnection _sqliteConnection;
    
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            ConfigureInMemorySqlite(context.Services);
        }
    
        private void ConfigureInMemorySqlite(IServiceCollection services)
        {
            // 建立链接并执行迁移。
            _sqliteConnection = CreateDatabaseAndGetConnection();
    
            // 使用 SQLite 作为 EF Provider。
            services.Configure<AbpDbContextOptions>(options =>
            {
                options.Configure(context =>
                {
                    context.DbContextOptions.UseSqlite(_sqliteConnection);
                });
            });
        }
    
        public override void OnApplicationShutdown(ApplicationShutdownContext context)
        {
            _sqliteConnection.Dispose();
        }
    
        private static SqliteConnection CreateDatabaseAndGetConnection()
        {
            // 使用 SQLite 的内存模式链接字符串。
            var connection = new SqliteConnection("Data Source=:memory:");
            connection.Open();
    
            var options = new DbContextOptionsBuilder<SampleMigrationsDbContext>()
                .UseSqlite(connection)
                .Options;
    
            // 执行迁移,构建表结构。
            using (var context = new SampleMigrationsDbContext(options))
            {
                context.GetService<IRelationalDatabaseCreator>().CreateTables();
            }
    
            return connection;
        }
    }
    

    总结

    ABP 的测试更偏向于集成测试,因为各个功能都依赖于模块,所以在执行单元测试的时候会运行更长的时间。日常开发过程当中,我们更多地还是针对应用层进行测试就可以了,粒度更细的话也可以针对仓储层领域层API 层 编写测试即可。

    为了保证项目质量,在开发完成之后编写单元/集成测试是每个开发人员应做的工作。编写单元/集成测试,虽然不能 100% 避免 BUG,但可以保证每次进行业务修改之后接口的正确性。

    四、总目录

    欢迎翻阅作者的其他文章,请 点击我 进行跳转,如果你觉得本篇文章对你有帮助,请点击文章末尾的 推荐按钮

    最后更新时间: 2021年6月27日 23点41分

  • 相关阅读:
    mysql 练习题
    mysql 语法
    mysql数据库简单练习(创建表格,增删改查数据)
    dom对象基础
    JS定时器
    JS小测验
    JS事件练习题
    JS事件
    dom对象
    tiles介绍
  • 原文地址:https://www.cnblogs.com/myzony/p/14708392.html
Copyright © 2011-2022 走看看