WIP composing customize attributes

This isn't really related to #751 - it's a refactor of some of
our test logic, to avoid bloat of semi-repeated autofixture
customization attributes.

By switching to a behaviour-driven model, it's possible to
compose them, which means that there's no need to part-duplicate
between them.  It should also reduce the overall number of them,
as combinations can be put together on-demand, rather than needing
a new attribute of their own.
This commit is contained in:
Craig Fowler 2021-04-02 11:08:00 +01:00
parent ef117eced7
commit d285b8b505
10 changed files with 257 additions and 99 deletions

View file

@ -1,83 +0,0 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Elsa.Core.IntegrationTests.Workflows;
using Elsa.Persistence.MongoDb.Extensions;
using Elsa.Testing.Shared.AutoFixture.Customizations;
using Microsoft.Extensions.DependencyInjection;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Elsa.Persistence.EntityFramework.Sqlite;
using Elsa.Persistence.YesSql;
using YesSql.Provider.Sqlite;
using System.Data;
using Elsa.Testing.Shared.Helpers;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public class HostBuilderWithDuplicateActivitiesWorkflowAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new HostBuilderUsingServicesCustomization(services => {
services
.AddElsa(elsa => {
elsa.AddWorkflow<DuplicateActivitiesWorkflow>();
});
}, parameter);
}
}
public class HostBuilderWithDuplicateActivitiesWorkflowAndMongoDbAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new HostBuilderUsingServicesCustomization(services => {
services
.AddElsa(elsa => {
elsa.AddWorkflow<DuplicateActivitiesWorkflow>();
elsa.UseMongoDbPersistence(opts => {
opts.ConnectionString = "mongodb://localhost:27017";
opts.DatabaseName = "IntegrationTests";
});
});
}, parameter);
}
}
public class HostBuilderWithDuplicateActivitiesWorkflowAndEntityFrameworkAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
var tempFolder = new TemporaryFolder();
return new HostBuilderUsingServicesCustomization(services => {
services
.AddElsa(elsa => {
elsa
.AddWorkflow<DuplicateActivitiesWorkflow>()
.UseEntityFrameworkPersistence(opts => {
opts.UseSqlite($"Data Source={tempFolder.Folder}elsa.db;", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name));
});
});
}, parameter);
}
}
public class HostBuilderWithDuplicateActivitiesWorkflowAndYesSqlAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new HostBuilderUsingServicesCustomization(services => {
services
.AddElsa(elsa => {
elsa
.AddWorkflow<DuplicateActivitiesWorkflow>()
.UseYesSqlPersistence(config => {
config.UseSqLite("Data Source=elsa-sqlite.db;", IsolationLevel.ReadUncommitted);
});
});
}, parameter);
}
}
}

View file

@ -0,0 +1,30 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Elsa.Core.IntegrationTests.Workflows;
using Elsa.Persistence.MongoDb.Extensions;
using Elsa.Testing.Shared.AutoFixture.Customizations;
using Microsoft.Extensions.DependencyInjection;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Elsa.Persistence.EntityFramework.Sqlite;
using Elsa.Persistence.YesSql;
using YesSql.Provider.Sqlite;
using Elsa.Testing.Shared.Helpers;
using Elsa.Testing.Shared.AutoFixture.Attributes;
using System;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public class WithDuplicateActivitiesWorkflowAttribute : ElsaHostBuilderBuilderCustomizeAttributeBase
{
public override Action<ElsaHostBuilderBuilder> GetBuilderCustomizer()
{
return builder => {
builder.ElsaCallbacks.Add(elsa => {
elsa.AddWorkflow<DuplicateActivitiesWorkflow>();
});
};
}
}
}

View file

@ -0,0 +1,25 @@
using System;
using Elsa.Testing.Shared.AutoFixture.Attributes;
using Elsa.Testing.Shared.Helpers;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Elsa.Persistence.EntityFramework.Sqlite;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public class WithEntityFrameworkAttribute : ElsaHostBuilderBuilderCustomizeAttributeBase
{
public override Action<ElsaHostBuilderBuilder> GetBuilderCustomizer()
{
var tempFolder = new TemporaryFolder();
return builder => {
builder.ElsaCallbacks.Add(elsa => {
elsa.UseEntityFrameworkPersistence(opts => {
opts.UseSqlite($"Data Source={tempFolder.GetContainedPath("elsa.db")};", db => db.MigrationsAssembly(typeof(SqliteElsaContextFactory).Assembly.GetName().Name));
});
});
};
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using Elsa.Persistence.MongoDb.Extensions;
using Elsa.Testing.Shared.AutoFixture.Attributes;
using Elsa.Testing.Shared.Helpers;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public class WithMongoDbAttribute : ElsaHostBuilderBuilderCustomizeAttributeBase
{
public override Action<ElsaHostBuilderBuilder> GetBuilderCustomizer()
{
return builder => {
builder.ElsaCallbacks.Add(elsa => {
elsa.UseMongoDbPersistence(opts => {
opts.ConnectionString = "mongodb://localhost:27017";
opts.DatabaseName = "IntegrationTests";
});
});
};
}
}
}

View file

@ -0,0 +1,25 @@
using Elsa.Testing.Shared.Helpers;
using Elsa.Testing.Shared.AutoFixture.Attributes;
using System;
using Elsa.Persistence.YesSql;
using YesSql.Provider.Sqlite;
using System.Data;
namespace Elsa.Core.IntegrationTests.Autofixture
{
public class WithSqliteYesSqlAttribute : ElsaHostBuilderBuilderCustomizeAttributeBase
{
public override Action<ElsaHostBuilderBuilder> GetBuilderCustomizer()
{
var tempFolder = new TemporaryFolder();
return builder => {
builder.ElsaCallbacks.Add(elsa => {
elsa.UseYesSqlPersistence(config => {
config.UseSqLite($"Data Source={tempFolder.GetContainedPath("elsa.db")};", IsolationLevel.ReadUncommitted);
});
});
};
}
}
}

View file

@ -7,6 +7,7 @@ using System.Threading;
using Elsa.Services;
using Elsa.Core.IntegrationTests.Workflows;
using Elsa.Persistence;
using Elsa.Testing.Shared.Helpers;
namespace Elsa.Core.IntegrationTests.Persistence
{
@ -20,35 +21,33 @@ namespace Elsa.Core.IntegrationTests.Persistence
*/
[Theory(DisplayName = "A workflow that contains duplicate activities may be run & persisted to an in-memory store"), AutoMoqData]
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableInMemory([HostBuilderWithDuplicateActivitiesWorkflow] IHostBuilder hostBuilder)
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableInMemory([WithDuplicateActivitiesWorkflow] ElsaHostBuilderBuilder hostBuilderBuilder)
{
hostBuilder.ConfigureServices((ctx, services) => {
services.AddHostedService<HostedWorkflowRunner<DuplicateActivitiesWorkflow>>();
});
var host = await hostBuilder.StartAsync();
await PerformTest(hostBuilderBuilder);
}
[Theory(DisplayName = "A workflow that contains duplicate activities may be run & persisted to an EF Sqlite store"), AutoMoqData]
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithEntityFramework([HostBuilderWithDuplicateActivitiesWorkflowAndEntityFramework] IHostBuilder hostBuilder)
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithEntityFramework([WithDuplicateActivitiesWorkflow,WithEntityFramework] ElsaHostBuilderBuilder hostBuilderBuilder)
{
hostBuilder.ConfigureServices((ctx, services) => {
services.AddHostedService<HostedWorkflowRunner<DuplicateActivitiesWorkflow>>();
});
var host = await hostBuilder.StartAsync();
await PerformTest(hostBuilderBuilder);
}
[Theory(DisplayName = "A workflow that contains duplicate activities may be run & persisted to a MongoDb store"), AutoMoqData]
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithMongoDb([HostBuilderWithDuplicateActivitiesWorkflowAndMongoDb] IHostBuilder hostBuilder)
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithMongoDb([WithDuplicateActivitiesWorkflow,WithMongoDb] ElsaHostBuilderBuilder hostBuilderBuilder)
{
hostBuilder.ConfigureServices((ctx, services) => {
services.AddHostedService<HostedWorkflowRunner<DuplicateActivitiesWorkflow>>();
});
var host = await hostBuilder.StartAsync();
await PerformTest(hostBuilderBuilder);
}
[Theory(DisplayName = "A workflow that contains duplicate activities may be run & persisted to a YesSQL store"), AutoMoqData]
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithYesSql([HostBuilderWithDuplicateActivitiesWorkflowAndYesSql] IHostBuilder hostBuilder)
public async Task ADuplicateActivitiesWorkflowInstanceShouldBeRoundTrippableWithYesSql([WithDuplicateActivitiesWorkflow,WithSqliteYesSql] ElsaHostBuilderBuilder hostBuilderBuilder)
{
await PerformTest(hostBuilderBuilder);
}
async Task PerformTest(ElsaHostBuilderBuilder hostBuilderBuilder)
{
var hostBuilder = hostBuilderBuilder.GetHostBuilder();
hostBuilder.ConfigureServices((ctx, services) => {
services.AddHostedService<HostedWorkflowRunner<DuplicateActivitiesWorkflow>>();
});

View file

@ -0,0 +1,64 @@
using System;
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;
using AutoFixture.Xunit2;
using Elsa.Testing.Shared.AutoFixture.Specifications;
using Elsa.Testing.Shared.Helpers;
namespace Elsa.Testing.Shared.AutoFixture.Attributes
{
public abstract class ElsaHostBuilderBuilderCustomizeAttributeBase : CustomizeAttribute
{
public abstract Action<ElsaHostBuilderBuilder> GetBuilderCustomizer();
public override ICustomization GetCustomization(ParameterInfo parameter)
=> new ElsaHostBuilderBuilderCustomization(GetBuilderCustomizer());
class ElsaHostBuilderBuilderCustomization : ICustomization
{
readonly Action<ElsaHostBuilderBuilder> builderCustomizer;
public void Customize(IFixture fixture)
{
fixture.Behaviors.Add(new ElsaHostBuilderBuilderTransformation(builderCustomizer));
}
public ElsaHostBuilderBuilderCustomization(Action<ElsaHostBuilderBuilder> builderCustomizer)
{
this.builderCustomizer = builderCustomizer ?? throw new ArgumentNullException(nameof(builderCustomizer));
}
}
class ElsaHostBuilderBuilderTransformation : ISpecimenBuilderTransformation
{
readonly Action<ElsaHostBuilderBuilder> builderCustomizer;
public ISpecimenBuilderNode Transform(ISpecimenBuilder builder)
{
return new Postprocessor(builder, new ElsaHostBuilderBuilderCommand(builderCustomizer), new IsAnElsaHostBuilderBuilderSpec());
}
public ElsaHostBuilderBuilderTransformation(Action<ElsaHostBuilderBuilder> builderCustomizer)
{
this.builderCustomizer = builderCustomizer ?? throw new ArgumentNullException(nameof(builderCustomizer));
}
}
class ElsaHostBuilderBuilderCommand : ISpecimenCommand
{
readonly Action<ElsaHostBuilderBuilder> builderCustomizer;
public void Execute(object specimen, ISpecimenContext context)
{
var builder = (ElsaHostBuilderBuilder) specimen;
builderCustomizer(builder);
}
public ElsaHostBuilderBuilderCommand(Action<ElsaHostBuilderBuilder> builderCustomizer)
{
this.builderCustomizer = builderCustomizer ?? throw new ArgumentNullException(nameof(builderCustomizer));
}
}
}
}

View file

@ -0,0 +1,10 @@
using AutoFixture.Kernel;
using Elsa.Testing.Shared.Helpers;
namespace Elsa.Testing.Shared.AutoFixture.Specifications
{
public class IsAnElsaHostBuilderBuilderSpec : IRequestSpecification
{
public bool IsSatisfiedBy(object request) => request is ElsaHostBuilderBuilder;
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.Testing.Shared.Helpers
{
/// <summary>
/// A helper class which 'builds' an <see cref="IHostBuilder"/> for testing purposes.
/// The reason this is useful is that it also allows building-up callbacks to occur inside
/// the <see cref="ElsaServiceCollectionExtensions.AddElsa"/> callback (which cannot be
/// done with a normal host builder alone).
/// </summary>
public class ElsaHostBuilderBuilder
{
/// <summary>
/// Gets a collection of the callbacks to be executed upon the <see cref="IServiceCollection"/> of
/// the host builder as it is created.
/// </summary>
/// <returns>The services callbacks</returns>
public IList<Action<IServiceCollection>> ServiceCallbacks { get; } = new List<Action<IServiceCollection>>();
/// <summary>
/// Gets a collection of the callbacks to be executed upon the <see cref="ElsaOptions"/> of
/// the host builder's services as it is created.
/// </summary>
/// <returns>The Elsa callbacks</returns>
public IList<Action<ElsaOptions>> ElsaCallbacks { get; } = new List<Action<ElsaOptions>>();
Action<ElsaOptions> ElsaConfiguration
=> ElsaCallbacks
.Where(x => !(x is null))
.Aggregate(EmptyElsaAction, (acc, next) => o => { acc(o); next(o); });
Action<IServiceCollection> ServiceConfiguration
=> ServiceCallbacks
.Where(x => !(x is null))
.Aggregate(EmptyServicesAction, (acc, next) => o => { acc(o); next(o); });
static Action<ElsaOptions> EmptyElsaAction => o => {};
static Action<IServiceCollection> EmptyServicesAction => s => {};
/// <summary>
/// Gets an <see cref="IHostBuilder"/> configured using the <see cref="ServiceCallbacks"/>
/// and <see cref="ElsaCallbacks"/>.
/// </summary>
/// <returns>A host builder.</returns>
public IHostBuilder GetHostBuilder()
{
return Host.CreateDefaultBuilder()
.ConfigureServices((hostBuilder, services) => {
services.AddElsa(ElsaConfiguration);
ServiceConfiguration(services);
});
}
}
}

View file

@ -15,6 +15,13 @@ namespace Elsa.Testing.Shared.Helpers
public string Folder { get; }
/// <summary>
/// Gets a path string for a file or folder which is contained within the <see cref="Folder"/>.
/// </summary>
/// <param name="relativePath">The relative path, within the temporary folder.</param>
/// <returns>A path for content within the temporary folder.</returns>
public string GetContainedPath(string relativePath) => Path.Combine(Folder, relativePath);
public void Dispose()
{
if (_deleteOnDispose)