我正在尝试进行集成测试来验证使用构造函数依赖项注入(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/