Skip to content

Commit

Permalink
Acceptance test support
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesRandall committed Mar 4, 2019
1 parent 3a7466b commit f02c900
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 1 deletion.
6 changes: 6 additions & 0 deletions FunctionMonkey.sln
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionMonkey.Commanding.C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCore21Example", "Samples\NetCore21Example\NetCore21Example.csproj", "{26B0ED78-AF78-48CD-B20D-D020A134469A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionMonkey.Testing", "Source\FunctionMonkey.Testing\FunctionMonkey.Testing.csproj", "{3B52E223-5530-4F25-85A0-ADFB9AFFB309}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -101,6 +103,10 @@ Global
{26B0ED78-AF78-48CD-B20D-D020A134469A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26B0ED78-AF78-48CD-B20D-D020A134469A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26B0ED78-AF78-48CD-B20D-D020A134469A}.Release|Any CPU.Build.0 = Release|Any CPU
{3B52E223-5530-4F25-85A0-ADFB9AFFB309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B52E223-5530-4F25-85A0-ADFB9AFFB309}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B52E223-5530-4F25-85A0-ADFB9AFFB309}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B52E223-5530-4F25-85A0-ADFB9AFFB309}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
100 changes: 100 additions & 0 deletions Source/FunctionMonkey.Testing/AbstractAcceptanceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.IO;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions.Builders;
using Microsoft.Extensions.DependencyInjection;

namespace FunctionMonkey.Testing
{
/// <summary>
/// A class that can be used as a basis for running acceptance tests with Function Monkey at the command dispatch level
/// designed for use with test frameworks that take a constructor approach to
///
/// One of the advantages of the pattern used by Function Monkey is that the host function triggers are separated cleanly
/// from business logic and compiled with pre-tested templates allowing for comprehensive acceptance tests to be run
/// just below this level which can often provide a high level of value with a lower level of complexity than also testing
/// the Function triggers.
///
/// Typically the none-generic version of this class is more useful but this version allows for a custom IFunctionHostBuilder
/// to be used if additional functionality is required over and above that baked into the supplied TestFunctionHostBuilder which
/// solely handles command registration and dependency injection management.
/// </summary>
public abstract class AbstractAcceptanceTest<TFunctionHostBuilder> where TFunctionHostBuilder : class, IFunctionHostBuilder
{
private readonly AcceptanceTestScaffold _scaffold = new AcceptanceTestScaffold();

protected AbstractAcceptanceTest()
{
_scaffold.Setup<TFunctionHostBuilder>(null, BeforeBuild, AfterBuild);
}

/// <summary>
/// Set up environment variables based on a settings.json file in a stream
/// </summary>
/// <param name="appSettings"></param>
protected void AddEnvironmentVariables(Stream appSettings, bool oneTimeOnly=true)
{
_scaffold.AddEnvironmentVariables(appSettings, oneTimeOnly);
}

/// <summary>
/// Set up environment variables based on a settings.json filename
/// </summary>
/// <param name="appSettingsPath"></param>
protected void AddEnvironmentVariables(string appSettingsPath, bool oneTimeOnly = true)
{
_scaffold.AddEnvironmentVariables(appSettingsPath, oneTimeOnly);
}

/// <summary>
/// This method can be used to modify dependency and command setup before the Function App Configuration
/// builder has been run and before tests are run.
///
/// This must not access members (and should not need to) as it is invoked from the constructor to
/// support test frameworks such as XUnit that construct test cases this way.
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="commandRegistry"></param>
public virtual void BeforeBuild(IServiceCollection serviceCollection, ICommandRegistry commandRegistry)
{

}

/// <summary>
/// This method can be used to modify dependency and command setup after the Function App Configuration
/// builder has been run and before tests are run.
///
/// This must not access members (and should not need to) as it is invoked from the constructor to
/// support test frameworks such as XUnit that construct test cases this way.
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="commandRegistry"></param>
public virtual void AfterBuild(IServiceCollection serviceCollection, ICommandRegistry commandRegistry)
{

}

/// <summary>
/// The constructed service provider
/// </summary>
public IServiceProvider ServiceProvider => _scaffold.ServiceProvider;

/// <summary>
/// A convenience property to provide easy access to the registered ICommandDispatcher
/// </summary>
public ICommandDispatcher Dispatcher => _scaffold.Dispatcher;
}

/// <summary>
/// A class that can be used as a basis for running acceptance tests with Function Monkey at the command dispatch level.
///
/// One of the advantages of the pattern used by Function Monkey is that the host function triggers are separated cleanly
/// from business logic and compiled with pre-tested templates allowing for comprehensive acceptance tests to be run
/// just below this level which can often provide a high level of value with a lower level of complexity than also testing
/// the Function triggers.
/// </summary>
public abstract class AbstractAcceptanceTest : AbstractAcceptanceTest<TestFunctionHostBuilder>
{

}
}
167 changes: 167 additions & 0 deletions Source/FunctionMonkey.Testing/AcceptanceTestScaffold.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using FunctionMonkey.Testing.Mocks;
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using AzureFromTheTrenches.Commanding;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions;
using FunctionMonkey.Abstractions.Builders;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;

namespace FunctionMonkey.Testing
{
/// <summary>
/// A scaffold class that can be used to set up Function Monkey acceptance tests designed for test frameworks that
/// make use of setup and teardown methods.
/// </summary>
public class AcceptanceTestScaffold
{
private static int _environmentVariablesRegistered = 0;

/// <summary>
/// Setup the scaffold with the default TestFunctionHostBuilder
/// </summary>
/// <param name="beforeBuild">An optional function to run before the Build method is called on the Function App configuration</param>
/// <param name="afterBuild">An optional function to run before the Build method is called after the Function App configuration</param>
/// <param name="functionAppConfigurationAssembly">If your Function App Configuration cannot be found you may need to provide the assembly it is located within to the setup - this is due to the as needed dependency loader and that a method setup based test may not yet have needed the required assembly.</param>
public void Setup(
Assembly functionAppConfigurationAssembly = null,
Action<IServiceCollection, ICommandRegistry> beforeBuild = null,
Action<IServiceCollection, ICommandRegistry> afterBuild = null)
{
Setup<TestFunctionHostBuilder>(functionAppConfigurationAssembly, beforeBuild, afterBuild);
}

/// <summary>
/// Setup the scaffold with a IFunctionHostBuilder
/// </summary>
/// <param name="beforeBuild">An optional function to run before the Build method is called on the Function App configuration</param>
/// <param name="afterBuild">An optional function to run before the Build method is called after the Function App configuration</param>
/// /// <param name="functionAppConfigurationAssembly">If your Function App Configuration cannot be found you may need to provide the assembly it is located within to the setup - this is due to the as needed dependency loader and that a method setup based test may not yet have needed the required assembly.</param>
public void Setup<TFunctionHostBuilder>(
Assembly functionAppConfigurationAssembly = null,
Action<IServiceCollection, ICommandRegistry> beforeBuild = null,
Action<IServiceCollection, ICommandRegistry> afterBuild = null
)
where TFunctionHostBuilder : class, IFunctionHostBuilder
{
IServiceCollection serviceCollection = new ServiceCollection();
RegisterFunctionMonkeyMocks(serviceCollection);
CommandingDependencyResolverAdapter adapter = new CommandingDependencyResolverAdapter(
(fromType, toInstance) => serviceCollection.AddSingleton(fromType, toInstance),
(fromType, toType) => serviceCollection.AddTransient(fromType, toType),
(resolveType) => ServiceProvider.GetService(resolveType)
);

// We register the commanding runtime on a per test / thread basis rather than globally
// as multiple tests running concurrently could be undertaking setup at the same time
// (we want this to be isolate)
CommandingRuntime commandingRuntime = new CommandingRuntime();
ICommandRegistry commandRegistry = commandingRuntime.AddCommanding(adapter);

IFunctionAppConfiguration functionAppConfiguration = functionAppConfigurationAssembly != null
? ConfigurationLocator.FindConfiguration(functionAppConfigurationAssembly)
: ConfigurationLocator.FindConfiguration();

IFunctionHostBuilder testFunctionHostBuilder =
(TFunctionHostBuilder) Activator.CreateInstance(
typeof(TFunctionHostBuilder),
BindingFlags.Default,
null,
new object[] {serviceCollection, commandRegistry},
null);

beforeBuild?.Invoke(serviceCollection, commandRegistry);

functionAppConfiguration.Build(testFunctionHostBuilder);

afterBuild?.Invoke(serviceCollection, commandRegistry);

ServiceProvider = serviceCollection.BuildServiceProvider();

Dispatcher = ServiceProvider.GetService<ICommandDispatcher>();
}

/// <summary>
/// Registers the internal dependencies of the Function Monkey runtime
/// </summary>
/// <param name="serviceCollection"></param>
protected virtual void RegisterFunctionMonkeyMocks(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<ICommandClaimsBinder, CommandClaimsBinderMock>();
serviceCollection.AddTransient<IContextSetter, ContextManagerMock>();
serviceCollection.AddTransient<IContextProvider, ContextManagerMock>();
}

/// <summary>
/// Add environment variables from a stream containing a Functions appsettings file
/// </summary>
/// <param name="appSettingsStream">The app settings stream</param>
/// <param name="oneTimeOnly">Defaults to true, if true only set the variables one time</param>
public void AddEnvironmentVariables(Stream appSettingsStream, bool oneTimeOnly = true)
{
if (Interlocked.Exchange(ref _environmentVariablesRegistered, 1) == 1)
{
return;
}

SetEnvironmentVariables(appSettingsStream);
}

/// <summary>
/// Add environment variables from a file containing a Functions appsettings file
/// </summary>
/// <param name="appSettingsPath">A path to the app settings file</param>
/// <param name="oneTimeOnly">Defaults to true, if true only set the variables one time</param>
public void AddEnvironmentVariables(string appSettingsPath, bool oneTimeOnly = true)
{
if (Interlocked.Exchange(ref _environmentVariablesRegistered, 1) == 1)
{
return;
}

using (Stream stream = new FileStream(appSettingsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
SetEnvironmentVariables(stream);
}
}

private static void SetEnvironmentVariables(Stream appSettings)
{
string json;
using (StreamReader reader = new StreamReader(appSettings))
{
json = reader.ReadToEnd();
}

if (!string.IsNullOrWhiteSpace(json))
{
JObject settings = JObject.Parse(json);
JObject values = (JObject)settings["Values"];
if (values != null)
{
foreach (JProperty property in values.Properties())
{
if (property.Value != null)
{
Environment.SetEnvironmentVariable(property.Name, property.Value.ToString());
}
}
}
}
}

/// <summary>
/// The constructed service provider
/// </summary>
public IServiceProvider ServiceProvider { get; private set; }

/// <summary>
/// A convenience property to provide easy access to the registered ICommandDispatcher
/// </summary>
public ICommandDispatcher Dispatcher { get; private set; }
}
}

26 changes: 26 additions & 0 deletions Source/FunctionMonkey.Testing/FunctionMonkey.Testing.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>0.21.6-beta000</Version>
<Authors>James Randall</Authors>
<Company>James Randall</Company>
<Copyright></Copyright>
<PackageLicenseUrl>https://raw.githubusercontent.com/JamesRandall/AzureFromTheTrenches.Commanding/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/JamesRandall/FunctionMonkey.git</RepositoryUrl>
<PackageProjectUrl>https://functionmonkey.azurefromthetrenches.com/</PackageProjectUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<AssemblyVersion>0.21.6.0</AssemblyVersion>
<FileVersion>0.21.6.0</FileVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FunctionMonkey.Abstractions\FunctionMonkey.Abstractions.csproj" />
<ProjectReference Include="..\FunctionMonkey\FunctionMonkey.csproj" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions Source/FunctionMonkey.Testing/Mocks/CommandClaimsBinderMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Security.Claims;
using AzureFromTheTrenches.Commanding.Abstractions;
using FunctionMonkey.Abstractions;

namespace FunctionMonkey.Testing.Mocks
{
public class CommandClaimsBinderMock : ICommandClaimsBinder
{
public bool Bind(ClaimsPrincipal principal, ICommand command)
{
return false;
}
}
}
Loading

0 comments on commit f02c900

Please sign in to comment.