c# - 如何使用或模拟 IWebJobsBuilder 对 Azure Function v2 进行集成测试?

标签 c# azure dependency-injection integration-testing azure-functions

我正在尝试进行集成测试来验证使用构造函数依赖项注入(inject)的最新 Azure Functions v2。

public sealed class CreateAccountFunction
{
    private readonly IAccountWorkflow m_accountWorkflow;

    private readonly ILogger<CreateAccountFunction> m_logger;

    private readonly IMapper m_mapper;

    public CreateAccountFunction(ILoggerFactory loggerFactory, IMapper mapper, IAccountWorkflow accountWorkflow)
    {
        m_logger = loggerFactory.CreateLogger<CreateAccountFunction>();
        m_mapper = mapper;
        m_accountWorkflow = accountWorkflow;
    }

    [FunctionName("CreateAccount")]
    public async Task<IActionResult> Run(
            [HttpTrigger(
                AuthorizationLevel.Function,
                "post",
                Route = "v1/accounts/"
            )]
            HttpRequest httpRequest)
    {
        //   Creates the account.
    }
}

我的 Startup 类包含以下内容:

public sealed class Startup : IWebJobsStartup
{
    public void Configure(IWebJobsBuilder webJobsBuilder)
    {
        webJobsBuilder.Services.AddLogging(loggingBuilder =>
        {
            loggingBuilder.SetMinimumLevel(LogLevel.Debug);
        });

        var mapperConfiguration = new MapperConfiguration(cfg => cfg.AddProfile(new ContractProfile()));
            webJobsBuilder.Services.AddSingleton(mapperConfiguration.CreateMapper());

        webJobsBuilder.Services.AddTransient<IAccountWorkflow, AccountWorkflow>();
    }
}

现在我想做一个Azure Function的集成测试。

public class CreateAccountFunctionTests
{
    private readonly CreateAccountFunction m_creationAccountFunction;


    public CreateAccountFunctionTests()
    {
        // --> How can I reuse the Startup and IWebJobsBuilder <--
        m_creationAccountFunction = new CreateAccountFunction(? ? ?);
    }

    [Fact]
    public void TestSomething()
    {
        // Arrange.
        HttpRequest httpRequest = /* builds an instance of HttpRequest */

        // Act.
        var result = m_creationAccountFunction.Run(httpRequest);

        // Assert.
        // Asserts the Status Code.
    }
}

问题

看起来很多注入(inject)内容都是由 IWebJobsBuilder 处理的。

我如何利用它来对我的 Azure Functions 进行集成测试?

我正在寻找一种解决方案,最大限度地减少创建自定义代码的需要并尽可能重用现有基础设施。

最佳答案

我查看了 Azure Function host code并在Program.cs中找到了这段代码文件:

var host = new HostBuilder()
                .SetAzureFunctionsEnvironment()
                .ConfigureLogging(b =>
                {
                    b.SetMinimumLevel(LogLevel.Information);
                    b.AddConsole();
                })
                .AddScriptHost(options, webJobsBuilder =>
                {
                    webJobsBuilder.AddAzureStorageCoreServices();
                })
                .UseConsoleLifetime()
                .Build();

让我感兴趣的部分是 AddScriptHost()扩展方法,这使得 webJobsBuilder实例( IWebJobsBuilder 的实现)可用。

知道这一点后,我创建了以下方法,该方法创建了一个简单的 IHost实例并使用我现有的 Startup包含所有注入(inject)服务的类:

/// <summary>
/// Builds an instance of the specified <typeparamref name="TFunctionType"/>
/// with the services defined in the <paramref name="startup"/> instance.
/// </summary>
/// <typeparam name="TFunctionType"></typeparam>
/// <param name="startup"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">
/// Thrown if:
/// - The <paramref name="startup" /> instance is not specified.
/// </exception>
public static TFunctionType Instanciate<TFunctionType>(Startup startup)
{
    Argument.ThrowIfIsNull(startup, nameof(startup));

    // --> Builds an IHost with all the services registered in the Startup.
    IHost host = new HostBuilder().ConfigureWebJobs(startup.Configure).Build();

    return Instanciate<TFunctionType>(host);
}

Instanciate<TFunctionType>方法查找 TFunctionType 的构造函数并从 IHost 检索所有服务实例:

/// <summary>
/// Instanciates the specified <typeparamref name="TFunctionType"></typeparamref>.
/// </summary>
/// <typeparam name="TFunctionType"></typeparam>
/// <param name="host"></param>
/// <returns></returns>
private static TFunctionType Instanciate<TFunctionType>(IHost host)
{
    Type type = typeof(TFunctionType);

    // --> This part could be better...
    ConstructorInfo contructorInfo = type.GetConstructors().FirstOrDefault();

    ParameterInfo[] parametersInfo = contructorInfo.GetParameters();

    object[] parameters = LookupServiceInstances(host, parametersInfo);

    return (TFunctionType) Activator.CreateInstance(type, parameters);
}

/// <summary>
/// Gets all the parameters instances from the host's services.
/// </summary>
/// <param name="host"></param>
/// <param name="parametersInfo"></param>
/// <returns></returns>
private static object[] LookupServiceInstances(IHost host, IReadOnlyList<ParameterInfo> parametersInfo)
{
    return parametersInfo.Select(p => host.Services.GetService(p.ParameterType))
                         .ToArray();
}

我将这些方法放在 HostHelper 中类(class)。现在,在我的测试中,我可以重复使用 Startup类(class)。

更好的是,我可以对 Startup 进行子类化这样我就可以模拟使用某种 I/O 的代码片段,使我的集成​​测试更具弹性:

public class CreateAccountFunctionTests
{
    private readonly CreateAccountFunction m_creationAccountFunction;

    public CreateAccountFunctionTests()
    {
        var startup = new Startup();

        m_creationAccountFunction = HostHelper.Instanciate<CreateAccountFunction>(startup);
    }

    [Fact]
    public void TestSomething()
    {
        // Arrange.
        HttpRequest httpRequest = /* builds an instance of HttpRequest */

        // Act.
        var result = m_creationAccountFunction.Run(httpRequest);

        // Assert.
        // Asserts the Status Code.
    }
}

更新

根据评论中的建议,我输入 the class on GitHub为了方便访问。这是完整的类:

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Hosting;

namespace NoSuchCompany.QualityTools.Service.Automation.Hosting
{
    #region Class

    /// <summary>
    /// Builds a <see cref="IHost"/> instance that can be used to inject parameters into a Function.
    /// </summary>
    /// <remarks>
    /// To use it for integration tests, first build a Startup class or one derived from it that contains
    /// mock instances of the services to inject.
    ///
    /// public class Startup
    /// {
    ///     public override void Configure(IFunctionsHostBuilder functionsHostBuilder)
    ///     {
    ///          ConfigureEmailService(functionsHostBuilder.Services);
    ///     }      
    ///
    /// 
    ///     protected virtual void ConfigureSomeService(IServiceCollection serviceCollection)
    ///     {
    ///        //  Inject a concrete service.
    ///        serviceCollection.AddTransient<ISomeService, SomeService>();
    ///     }
    /// }
    /// 
    /// public sealed class TestStartup : Startup
    /// {
    ///     protected override void ConfigureSomeService(IServiceCollection serviceCollection)
    ///     {
    ///        //  Inject a mock service.
    ///        serviceCollection.AddTransient<ISomeService, MockOfSomeService>();
    ///     }
    /// }
    ///
    /// Then, the helper can be called with like this:
    ///
    /// var startup = new TestStartup();
    /// 
    /// var myAzureFunctionToTest = HostHelper.Instantiate<AnAzureFunction>(startup);
    /// 
    /// </remarks>
    [ExcludeFromCodeCoverage]
    public static class HostHelper
    {
        #region Public Methods

        /// <summary>
        /// Builds an instance of the specified <typeparamref name="TFunctionType"/>
        /// with the services defined in the <paramref name="startup"/> instance.
        /// </summary>
        /// <typeparam name="TFunctionType"></typeparam>
        /// <param name="startup"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// Thrown if:
        /// - The <paramref name="startup" /> instance is not specified.
        /// </exception>
        public static TFunctionType Instantiate<TFunctionType>(Startup startup)
        {
            if(startup is null)
                throw new ArgumentNullException($"The parameter {nameof(startup)} instance is not specified.");

            IHost host = new HostBuilder().ConfigureWebJobs(startup.Configure).Build();

            return Instantiate<TFunctionType>(host);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Instantiates the specified <typeparamref name="TFunctionType"></typeparamref>.
        /// </summary>
        /// <typeparam name="TFunctionType"></typeparam>
        /// <param name="host"></param>
        /// <returns></returns>
        private static TFunctionType Instantiate<TFunctionType>(IHost host)
        {
            Type type = typeof(TFunctionType);

            ConstructorInfo constructorInfo = type.GetConstructors().FirstOrDefault();

            ParameterInfo[] parametersInfo = constructorInfo.GetParameters();

            object[] parameters = LookupServiceInstances(host, parametersInfo);

            return (TFunctionType) Activator.CreateInstance(type, parameters);
        }

        /// <summary>
        /// Gets all the parameters instances from the host's services.
        /// </summary>
        /// <param name="host"></param>
        /// <param name="parametersInfo"></param>
        /// <returns></returns>
        private static object[] LookupServiceInstances(IHost host, IReadOnlyList<ParameterInfo> parametersInfo)
        {
            return parametersInfo.Select(parameter => host.Services.GetService(parameter.ParameterType))
                                 .ToArray();
        }

        #endregion
    }

    #endregion
}

关于c# - 如何使用或模拟 IWebJobsBuilder 对 Azure Function v2 进行集成测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55315785/

相关文章:

java - 加载静态属性文件类

android - Dagger 是否支持 ActivityInstrumentationTestCase2 测试的依赖注入(inject)

c# - libtool:您应该使用 libtool 2.4.6 中的宏重新创建 aclocal.m4

c# - ViewModel 的版本控制

C# 复制 powerpoint 主幻灯片布局

powershell - 由于 Azure Key Vault 内部错误,具有 Bit-locker 的 Azure VM 现在无法解密?

java - 何时何地使用 Guice 依赖注入(inject)?

c# - 连续接收来自秤的数据

Azure云服务部署

azure - 加密迁移数据库