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(); } }