当前位置:网站首页>use Xunit.DependencyInjection Transformation test project

use Xunit.DependencyInjection Transformation test project

2020-11-07 20:15:30 Weihanli.

Use Xunit.DependencyInjection Transformation test project

Intro

This article has been delayed for a long time , It has been introduced before Xunit.DependencyInjection This project , This project was written by a master Xunit Based on Microsoft GenericHost and An extension library for dependency injection implementation , It can make it easier for you to implement dependency injection in test projects , And I think another good point is that it can better control the operation process , For example, many initialization operations are done before starting the test , Better process control .

Recently, most of our company's testing projects are based on Xunit.DependencyInjection Transformed , The effect is very good .

Recently, I started my test project manually from the original one Web Host It's based on Xunit.DepdencyInjection To use , At the same time, it is also preparing for the update of integration test of a project of our company , It's delicious to use ~

I think Xunit.DependencyInjection Solved my two big pain points , One is that dependency injection code doesn't write well , One is a simpler process control process , Here is a general introduction to

XUnit.DependencyInjection Workflow

Xunit.DepdencyInjection The main process is DependencyInjectionTestFramework in , See https://github.com/pengweiqhca/Xunit.DependencyInjection/blob/7.0/Xunit.DependencyInjection/DependencyInjectionTestFramework.cs

First of all, I will try to find the... In the project Startup , This Startup Very similar to asp.net core Medium Startup, Almost exactly , It's just a little different , Startup Dependency injection is not supported , Can not be like asp.net core Put in a like that IConfiguration Object to get the configuration , besides , and asp.net core Of Startup Have the same experience , If you can't find this Startup There are no services or special configurations that need dependency injection , Use it directly Xunit The original XunitTestFrameworkExecutor, If you find it Startup From Startup Configure in the agreed method Host, Registration service and initialization configuration process , Finally using DependencyInjectionTestFrameworkExecutor Carry out our test case.

The source code parsing

The source code uses C#8 Some of the new grammar of , The code is very simple , The following code uses nullable reference types :

DependencyInjectionTestFramework Source code

public sealed class DependencyInjectionTestFramework : XunitTestFramework
{
    public DependencyInjectionTestFramework(IMessageSink messageSink) : base(messageSink) { }

    protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
    {
        IHost? host = null;
        try
        {
            //  obtain  Startup  example 
            var startup = StartupLoader.CreateStartup(StartupLoader.GetStartupType(assemblyName));
            if (startup == null) return new XunitTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
            //  establish  HostBuilder
            var hostBuilder = StartupLoader.CreateHostBuilder(startup, assemblyName) ??
                                new HostBuilder().ConfigureHostConfiguration(builder =>
                                    builder.AddInMemoryCollection(new Dictionary<string, string> { { HostDefaults.ApplicationKey, assemblyName.Name } }));
            //  call  Startup  Medium  ConfigureHost  Method configuration  Host
            StartupLoader.ConfigureHost(hostBuilder, startup);
            //  call  Startup  Medium  ConfigureServices  Method register service 
            StartupLoader.ConfigureServices(hostBuilder, startup);
            //  Register default service , structure  Host
            host = hostBuilder.ConfigureServices(services => services
                    .AddSingleton(DiagnosticMessageSink)
                    .TryAddSingleton<ITestOutputHelperAccessor, TestOutputHelperAccessor>())
                .Build();
            //  call  Startup  Medium  Configure  Method to initialize 
            StartupLoader.Configure(host.Services, startup);
            //  return  testcase executor, Ready to start running test cases 
            return new DependencyInjectionTestFrameworkExecutor(host, null,
                assemblyName, SourceInformationProvider, DiagnosticMessageSink);
        }
        catch (Exception e)
        {
            return new DependencyInjectionTestFrameworkExecutor(host, e,
                assemblyName, SourceInformationProvider, DiagnosticMessageSink);
        }
    }
}

StarpupLoader Source code

public static Type? GetStartupType(AssemblyName assemblyName)
{
    var assembly = Assembly.Load(assemblyName);
    var attr = assembly.GetCustomAttribute<StartupTypeAttribute>();

    if (attr == null) return assembly.GetType($"{assemblyName.Name}.Startup");

    if (attr.AssemblyName != null) assembly = Assembly.Load(attr.AssemblyName);

    return assembly.GetType(attr.TypeName) ?? throw new InvalidOperationException($"Can't load type {attr.TypeName} in '{assembly.FullName}'");
}

public static object? CreateStartup(Type? startupType)
{
    if (startupType == null) return null;

    var ctors = startupType.GetConstructors();
    if (ctors.Length != 1 || ctors[0].GetParameters().Length != 0)
        throw new InvalidOperationException($"'{startupType.FullName}' must have a single public constructor and the constructor without parameters.");

    return Activator.CreateInstance(startupType);
}

public static IHostBuilder? CreateHostBuilder(object startup, AssemblyName assemblyName)
{
    var method = FindMethod(startup.GetType(), nameof(CreateHostBuilder), typeof(IHostBuilder));
    if (method == null) return null;

    var parameters = method.GetParameters();
    if (parameters.Length == 0)
        return (IHostBuilder)method.Invoke(startup, Array.Empty<object>());

    if (parameters.Length > 1 || parameters[0].ParameterType != typeof(AssemblyName))
        throw new InvalidOperationException($"The '{method.Name}' method of startup type '{startup.GetType().FullName}' must without parameters or have the single 'AssemblyName' parameter.");

    return (IHostBuilder)method.Invoke(startup, new object[] { assemblyName });
}

public static void ConfigureHost(IHostBuilder builder, object startup)
{
    var method = FindMethod(startup.GetType(), nameof(ConfigureHost));
    if (method == null) return;

    var parameters = method.GetParameters();
    if (parameters.Length != 1 || parameters[0].ParameterType != typeof(IHostBuilder))
        throw new InvalidOperationException($"The '{method.Name}' method of startup type '{startup.GetType().FullName}' must have the single 'IHostBuilder' parameter.");

    method.Invoke(startup, new object[] { builder });
}

public static void ConfigureServices(IHostBuilder builder, object startup)
{
    var method = FindMethod(startup.GetType(), nameof(ConfigureServices));
    if (method == null) return;

    var parameters = method.GetParameters();
    builder.ConfigureServices(parameters.Length switch
    {
        1 when parameters[0].ParameterType == typeof(IServiceCollection) =>
        (context, services) => method.Invoke(startup, new object[] { services }),
        2 when parameters[0].ParameterType == typeof(IServiceCollection) &&
                parameters[1].ParameterType == typeof(HostBuilderContext) =>
        (context, services) => method.Invoke(startup, new object[] { services, context }),
        2 when parameters[1].ParameterType == typeof(IServiceCollection) &&
                parameters[0].ParameterType == typeof(HostBuilderContext) =>
        (context, services) => method.Invoke(startup, new object[] { context, services }),
        _ => throw new InvalidOperationException($"The '{method.Name}' method in the type '{startup.GetType().FullName}' must have a 'IServiceCollection' parameter and optional 'HostBuilderContext' parameter.")
    });
}

public static void Configure(IServiceProvider provider, object startup)
{
    var method = FindMethod(startup.GetType(), nameof(Configure));

    method?.Invoke(startup, method.GetParameters().Select(p => provider.GetService(p.ParameterType)).ToArray());
}

Actual case

unit testing

Let's look at a modification of a unit test in our project , Before the transformation, it was like this :

This test project uses an older version of AutoMapper, Each has to use AutoMapper You need to call the registration in the test case AutoMapper mapping Relationship method to register mapping Relationship , because Register Method called directly in the Mapper.Initialize Methods registration mapping Relationship , Multiple calls will throw an exception , So every test case method uses AutoMapper All of them have this disgusting logic

The first revision , I am here Register Method to make a simple transformation , hold try...catch Removed :

But it's still uncomfortable , Each uses AutoMapper We still need to call Register Method

Use Xunit.DepdencyInjection After that, you can just Startup Medium Configure Register in the method , Just call it once

We'll put AutoMapper The upgrade , Use the dependency injection pattern to use AutoMapper, Use after modification

Inject the required service directly into the class of the test case IMapper that will do

Integration testing

Integration testing is similar , Integration testing I use my own project as an example

My integration test project was originally developed with xunit Inside CollectionFixture combination WebHost To achieve ( from 2.2 Updated ,), stay .net core 3.1 It can be configured directly WebHostedService That's all right. , and Xunit.DependencyInjection Is based on Microsoft GenericHost So , It will also be easier to do integration .

stay Startup in adopt ConfigureHost Method configuration IHostBuilder Extension method of ConfigureWebHost , Registration test required services , Inject services into the constructor of the test sample class

Integration test modification changes can refer to : https://github.com/OpenReservation/ReservationServer/commit/d30e35116da0b8d4bf3e65f0a1dcabcad8fecae0

Startup Supported methods

  • CreateHostBuilder
public class Startup
{
    public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { }
}

Use this method to customize IHostBuilder You can use this method when , This method may not be used very often , Can pass ConfigureHost Method to configure Host

The default is direct new HostBuilder(), Want to build aspnet.core It is configured by default HostBuilder, have access to Host.CreateDefaultBuilder() To create IHostBuilder

  • ConfigureHost To configure Host
public class Startup
{
    public void ConfigureHost(IHostBuilder hostBuilder) { }
}

adopt ConfigureHost To configure the Host, You can configure IConfiguration, You can also configure the services to be registered

Configuration can be done through IHostBuilder Extension method of ConfigureAppConfiguration To update the configuration

  • ConfigureServices
public class Startup
{
    public void ConfigureServices(IServiceCollection services[, HostBuilderContext context]) { }
}

If you don't need to read IConfiguration It can be used directly ConfigurationServices(IServiceCollection services) Method

If you need to read IConfiguration, Can pass ConfigureServices(IServiceCollection services, HostBuilderContext context) Methods by HostBuilderContext.Configuration To access the configuration object IConfiguration

  • Configure
public class Startup
{
    public void Configure([IServiceProvider applicationServices]) { }
}

Configure Methods can have no parameters , It also supports all injected Services , and asp.net core Inside Configure The method is similar to , You can usually do some initialization configuration in this method

More

If you're using Xunit When you encounter the above problems , I recommend you try Xunit.DependenceInjection This project , It's worth a try ~~

Reference

版权声明
本文为[Weihanli.]所创,转载请带上原文链接,感谢