using Elsa.Common;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.CommitStates;
using Elsa.Workflows.Management.Providers;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Memory;
using Elsa.Workflows.PortResolvers;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
namespace Elsa.Testing.Shared;
///
/// A test fixture for unit testing activities in isolation.
/// Provides a fluent API to configure services, variables, and execution context.
///
public class ActivityTestFixture
{
private Action? _configureContextAction;
///
/// Initializes a new instance of the class.
///
/// The activity to test
public ActivityTestFixture(IActivity activity)
{
Activity = activity;
Services = new ServiceCollection();
AddCoreWorkflowServices(Services);
}
///
/// Represents the activity being tested within the context of the activity test fixture.
/// Provides access to the activity for configuration, execution, and validation purposes.
///
public IActivity Activity { get; }
///
/// Gets the service collection for registering additional services.
/// Use this to add services required by the activity under test.
///
[UsedImplicitly]
public IServiceCollection Services { get; private set; }
///
/// Configures the service collection using a fluent action.
///
/// Action to configure the service collection
/// The fixture instance for method chaining
public ActivityTestFixture ConfigureServices(Action configure)
{
configure(Services);
return this;
}
///
/// Configures the activity execution context before execution.
/// Multiple calls to this method will chain the configuration actions together.
///
/// Action to configure the activity execution context
/// The fixture instance for method chaining
[UsedImplicitly]
public ActivityTestFixture ConfigureContext(Action configure)
{
_configureContextAction += configure;
return this;
}
///
/// Executes the activity and returns the execution context.
///
/// The ActivityExecutionContext after execution
public async Task ExecuteAsync()
{
var context = await BuildAsync();
// Set up variables and inputs, then execute the activity
await SetupExistingVariablesAsync(Activity, context);
await context.EvaluateInputPropertiesAsync();
context.TransitionTo(ActivityStatus.Running);
await Activity.ExecuteAsync(context);
return context;
}
///
/// Builds the ActivityExecutionContext without executing the activity.
///
public async Task BuildAsync()
{
var serviceProvider = Services.BuildServiceProvider();
var activityRegistry = serviceProvider.GetRequiredService();
var workflowGraphBuilder = serviceProvider.GetRequiredService();
await activityRegistry.RegisterAsync(Activity.GetType());
var workflow = Workflow.FromActivity(Activity);
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
// Create workflow execution context using the static factory method
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(
serviceProvider,
workflowGraph,
$"test-instance-{Guid.NewGuid()}",
CancellationToken.None
);
// Create ActivityExecutionContext for the actual activity we want to test
var context = await workflowExecutionContext.CreateActivityExecutionContextAsync(Activity);
// Apply any context configuration action
_configureContextAction?.Invoke(context);
return context;
}
///
/// Sets up existing variables found on the activity in the execution context.
/// This is necessary because in unit tests, variables need to be initialized.
///
private static Task SetupExistingVariablesAsync(IActivity activity, ActivityExecutionContext context)
{
var activityType = activity.GetType();
var variableProperties = activityType.GetProperties()
.Where(p => typeof(Variable).IsAssignableFrom(p.PropertyType))
.ToList();
foreach (var variable in variableProperties.Select(property => (Variable?)property.GetValue(activity)))
{
if(variable == null)
continue;
context.WorkflowExecutionContext.MemoryRegister.Declare(variable);
variable.Set(context.ExpressionExecutionContext, variable.Value);
}
return Task.CompletedTask;
}
private static void AddCoreWorkflowServices(IServiceCollection services)
{
services.AddLogging();
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton();
services.AddScoped();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton();
services.AddSingleton(_ => Substitute.For());
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
}
}