更優雅的在 Xunit 中使用依賴注入

WeihanLi發表於2020-07-16

Xunit.DependencyInjection 7.0 釋出了

Intro

上次我們已經介紹過一次大師的 Xunit.DependencyInjection https://www.cnblogs.com/weihanli/p/xuint-dependency-injection.html ,最近大師完成了 7.0 的重構並且已經正式釋出,已經可以直接安裝使用了

7.0 為我們帶來了更好的程式設計體驗,在 6.x 的版本中,我們的 Startup 需要繼承於 DependencyInjectionTestFramework 而且需要設定一個 assembly attribute,這在 7.0 中都不需要了,下面我們來看看有了哪些變化

Startup 的變化

首先來看大師給出的 diff


-[assembly: TestFramework("Your.Test.Project.Startup", "Your.Test.Project")]

namespace Your.Test.Project
{
-   public class Startup : DependencyInjectionTestFramework
+   public class Startup
    {
-       public Startup(IMessageSink messageSink) : base(messageSink) { }

-       protected void ConfigureServices(IServiceCollection services)
+       public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IDependency, DependencyClass>();
        }

-       protected override IHostBuilder CreateHostBuilder() =>
-           base.CreateHostBuilder(assemblyName)
-               .ConfigureServices(ConfigureServices);

-       protected override void Configure(IServiceProvider provider)
+       public void Configure(IServiceProvider provider)
    }
}
  1. 移除了 TestFramework assembly attribute
  2. 不再需要繼承於 DependencyInjectionTestFramework
  3. 也因為上面的不需要繼承,所以原本要 override 的方法可以不 override 了,原來是 protected 的方法現在需要改成 public

這樣改了之後首先我們在使用的時候無需知道 DependencyInjectionTestFramework 的存在了,而且可以更符合 asp.net core Startup 的使用習慣,可以遮蔽掉很多實現細節,使用者只需要在 Startup 註冊自己的邏輯即可,更為專注於自己的邏輯而無需關心框架所做的事情

新的 Startup 解析

我把上一篇文章寫的示例用升級到了新的版本,下面是更新後的示例程式碼

namespace XUnitDependencyInjectionSample
{
    public class Startup
    {
        // 自定義 HostBuilder ,可以沒有這個方法,沒有這個方法會使用預設的 hostBuilder,通常直接使用 `ConfigureHost` 應該就夠用了
        // public IHostBuilder CreateHostBuilder()
        // {
        //     return new HostBuilder()
        //         .ConfigureAppConfiguration(builder =>
        //         {
        //             // 註冊配置
        //             builder
        //                 .AddInMemoryCollection(new Dictionary<string, string>()
        //                 {
        //                     {"UserName", "Alice"}
        //                 })
        //                 .AddJsonFile("appsettings.json")
        //                 ;
        //         })
        //         .ConfigureServices((context, services) =>
        //         {
        //             // 註冊自定義服務
        //             services.AddSingleton<IIdGenerator, GuidIdGenerator>();
        //             if (context.Configuration.GetAppSetting<bool>("XxxEnabled"))
        //             {
        //                 services.AddSingleton<IUserIdProvider, EnvironmentUserIdProvider>();
        //             }
        //         })
        //         ;
        // }

        // 自定義 host 構建
        public void ConfigureHost(IHostBuilder hostBuilder)
        {
            hostBuilder
                .ConfigureAppConfiguration(builder =>
                {
                    // 註冊配置
                    builder
                        .AddInMemoryCollection(new Dictionary<string, string>()
                        {
                            {"UserName", "Alice"}
                        })
                        .AddJsonFile("appsettings.json")
                        ;
                })
                .ConfigureServices((context, services) =>
                {
                    // 註冊自定義服務
                    services.AddSingleton<IIdGenerator, GuidIdGenerator>();
                    if (context.Configuration.GetAppSetting<bool>("XxxEnabled"))
                    {
                        services.AddSingleton<IUserIdProvider, EnvironmentUserIdProvider>();
                    }
                })
                ;
        }

        // 支援的形式:
        // ConfigureServices(IServiceCollection services)
        // ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext)
        // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
        public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext)
        {
            services.TryAddSingleton<CustomService>();
        }

        // 可以新增要用到的方法引數,會自動從註冊的服務中獲取服務例項,類似於 asp.net core 裡 Configure 方法
        public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator)
        {
            // 有一些測試資料要初始化可以放在這裡
            // InitData();
        }
    }
}

在新的版本中 Startup 和 asp.net core 裡的 Startup 更加相像了,

會多一個 CreateHostBuilder/ConfigureHost(IHostBuilder) 的方法,允許使用者自定義 Host 的構建,也可以沒有這個方法

ConfigureServices 方法允許使用者增加 HostBuilderContext 作為引數,可以通過 hostBuilderContext 來獲取配置資訊,也可以在 CreateHostBuilder/ConfigureHost(IHostBuilder) 裡註冊也是一樣的

註冊配置/服務和 asp.net core 裡一模一樣,有資料或配置需要在專案啟動時初始化的,可以放在 Configure 方法做,有點類似於 asp.net core 裡 Startup 中的 Configure 方法,可以將需要的服務作為方法引數,執行時會自動從註冊的服務中獲取

Startup 的尋找方法

預設的 Startup 通常是 ProjectName.Startup,通常在專案根目錄下建立一個 Startup 是不需要配置的,如果不是或不起作用,可以參考下面 Startup 的尋找規則

如果要使用一個特別的 Startup, 你可以通過在專案檔案的 PropertyGroup 部分定義 XunitStartupAssemblyXunitStartupFullName,具體規則如下

<Project>
  <PropertyGroup>
    <XunitStartupAssembly>Abc</XunitStartupAssembly>
    <XunitStartupFullName>Xyz</XunitStartupFullName>
  </PropertyGroup>
</Project>
XunitStartupAssembly XunitStartupFullName Startup
Your.Test.Project.Startup, Your.Test.Project
Abc Abc.Startup, Abc
Xyz Xyz, Your.Test.Project
Abc Xyz Xyz, Abc

More

除了上面的 Startup 的改動之外,新版本還支援了 xunit 中 fixture 的依賴注入,似乎是由一個外國小哥提的 PR, 詳見:https://github.com/pengweiqhca/Xunit.DependencyInjection/pull/21

有了這個神器,在測試程式碼中使用依賴注入要方便很多了,還沒有用起來的可以準備上手了~~

Reference

相關文章