using Elsa.Common;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows;
using Elsa.Workflows.CommitStates;
using Elsa.Workflows.Management.Providers;
using Elsa.Workflows.Management.Services;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
namespace Elsa.Activities.UnitTests.Helpers;
///
/// Helper class for unit testing activities in isolation.
/// Provides methods to execute activities with minimal setup, abstracting away the complexity
/// of creating WorkflowExecutionContext and ActivityExecutionContext.
///
public static class ActivityTestHelper
{
///
/// Executes an activity in isolation for unit testing purposes and returns the execution context.
/// This method handles all the complexity of setting up the execution context,
/// evaluating inputs, and executing the activity.
///
/// The activity to execute
/// The ActivityExecutionContext used for execution
public static async Task ExecuteActivityAsync(IActivity activity)
{
var context = CreateMinimalActivityExecutionContext(activity, out var serviceProvider);
// Set up variables and inputs, then execute the activity
await SetupExistingVariablesAsync(activity, context);
await SetupInputValuesInMemoryAsync(activity, context, serviceProvider);
await context.EvaluateInputPropertiesAsync();
await activity.ExecuteAsync(context);
return context;
}
///
/// Creates a minimal ActivityExecutionContext suitable for isolated unit testing of activities.
/// This helper method creates a real WorkflowExecutionContext using the minimal workflow pattern
/// to provide proper context for activities.
///
private static ActivityExecutionContext CreateMinimalActivityExecutionContext(IActivity activity, out IServiceProvider serviceProvider)
{
// Create a minimal service provider with the required services for expression evaluation
var services = new ServiceCollection();
// Add core services
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton();
// Add real expression evaluation services instead of mocks
services.AddScoped();
// Add the well-known type registry required by expression handlers
services.AddSingleton();
// Add the default expression descriptor provider which includes Literal expressions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(_ => Substitute.For());
// Mock the complex workflow-level dependencies
services.AddSingleton(_ => Substitute.For());
// Set up the activity registry lookup service to return proper descriptors
var activityRegistryLookup = Substitute.For();
activityRegistryLookup.FindAsync(Arg.Any(), Arg.Any()).Returns(callInfo =>
{
var activityType = callInfo.ArgAt(0);
return Task.FromResult(new ActivityDescriptor
{
TypeName = activityType,
Kind = ActivityKind.Action,
Category = "Test",
Description = "Test activity for unit testing",
Version = 1
});
});
services.AddSingleton(_ => activityRegistryLookup);
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
serviceProvider = services.BuildServiceProvider();
// Create a minimal workflow
activity.Id ??= $"test-activity-{Guid.NewGuid()}";
var workflow = new Workflow
{
Root = activity
};
// Create a simple workflow graph manually instead of using the builder
var rootNode = new ActivityNode(activity, "Root");
var nodes = new List { rootNode };
var workflowGraph = new WorkflowGraph(workflow, rootNode, nodes);
// Create workflow execution context using the static factory method
var workflowExecutionContext = WorkflowExecutionContext.CreateAsync(
serviceProvider,
workflowGraph,
$"test-instance-{Guid.NewGuid()}",
CancellationToken.None
).GetAwaiter().GetResult();
// Create ActivityExecutionContext for the actual activity we want to test
var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContextAsync(activity)
.GetAwaiter().GetResult();
return activityExecutionContext;
}
///
/// 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 => p.PropertyType.IsGenericType &&
p.PropertyType.BaseType == typeof(Variable))
.ToList();
foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!))
{
variable.Set(context.ExpressionExecutionContext, variable.Value);
}
return Task.CompletedTask;
}
///
/// Sets up input values in memory blocks so that context.Get() can find them during activity execution.
/// This mimics what the workflow engine does when evaluating inputs.
///
private static async Task SetupInputValuesInMemoryAsync(IActivity activity, ActivityExecutionContext context, IServiceProvider serviceProvider)
{
var activityType = activity.GetType();
var inputProperties = activityType.GetProperties()
.Where(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(Input<>))
.ToList();
foreach (var input in inputProperties.Select(property => property.GetValue(activity) as Input))
{
if (input?.Expression == null)
{
continue;
}
// Get the memory block reference for this input
var memoryBlockReference = input.MemoryBlockReference();
// Evaluate the input using the expression evaluator
var expressionEvaluator = serviceProvider.GetService();
var evaluatedValue = await expressionEvaluator!.EvaluateAsync(input, context.ExpressionExecutionContext);
// Set the value in the memory block
memoryBlockReference.Set(context, evaluatedValue);
}
}
}