簡體   English   中英

如何使用或模擬 IWebJobsBuilder 對 Azure Function v2 進行集成測試?

[英]How can I use or mock IWebJobsBuilder to do an integration test of my Azure Function v2?

我正在嘗試進行集成測試以驗證使用構造函數依賴項注入的最新 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.
    }
}

看起來很多注入的東西都是由IWebJobsBuilder處理的。

如何利用它對我的 Azure Functions 進行集成測試?

我正在尋找一種解決方案,可以最大限度地減少創建自定義代碼的需求並盡可能重用現有基礎設施。

我查看了Azure Function 主機代碼,並在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類:

/// <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>方法查找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類中。 現在,在我的測試中,我可以重用Startup類。

更好的是,我可以將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.
    }
}

更新

正如評論中所建議的,我將課程放在 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
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM