From 4a26819ed712d5f7a2e83aa6bbc7545ebe7161a0 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 7 Oct 2025 15:56:08 +0200 Subject: [PATCH 01/11] (WIP) Initial context helper + provisional unit tests for setvariable --- Elsa.sln | 9 +- .../Elsa.Activities.UnitTests.csproj | 11 + .../Primitives/SetVariableTests.cs | 217 ++++++++++++++++++ test/unit/Elsa.Activities.UnitTests/Usings.cs | 3 + 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj create mode 100644 test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs create mode 100644 test/unit/Elsa.Activities.UnitTests/Usings.cs diff --git a/Elsa.sln b/Elsa.sln index 1a2bd9126..3ddca6a56 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -298,6 +298,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EFCore.Sql EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.IntegrationTests", "test\integration\Elsa.Alterations.IntegrationTests\Elsa.Alterations.IntegrationTests.csproj", "{51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -602,6 +604,10 @@ Global {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9}.Release|Any CPU.Build.0 = Release|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -628,7 +634,6 @@ Global {5948B0A5-7873-4DBB-BA03-EB283D6EA91B} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {B08B4E00-C2AB-48F3-8389-449F42AEF179} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {28818676-F6AF-4203-8B65-BD33A50CB9A2} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} - {DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92} {29638A67-E79F-44FE-AC05-DA499EBA929E} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {BBCE36D1-6767-4ED1-B3E8-84D2567A962A} = {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} @@ -695,6 +700,8 @@ Global {71D5178D-2490-4681-8621-BF8DED964F33} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {698051E0-7981-43D4-B7BA-F3D8B65004A1} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92} + {2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj new file mode 100644 index 000000000..3c3dec1fb --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs new file mode 100644 index 000000000..db4b90d02 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -0,0 +1,217 @@ +using Elsa.Common; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Mediator.Contracts; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.CommitStates; +using Elsa.Workflows.Memory; +using Elsa.Workflows.Models; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Elsa.Activities.UnitTests.Primitives; + +public class SetVariableTests +{ + [Fact] + public async Task Should_Set_Variable() + { + // Arrange + var variable = new Variable("myVar", 0); + var setVariable = new SetVariable(variable, new Input(42)); + var context = CreateMinimalActivityExecutionContext(setVariable); + + // Prepare the activity inputs in the context + //PrepareActivityInputs(setVariable, context); + + // Act + await ExecuteActivityAsync(setVariable, context); + + // Assert + var result = variable.Get(context); + Assert.Equal(42, result); + } + + [Fact] + public async Task Should_Set_Variable_From_Expression() + { + // Arrange + var variable = new Variable("myStringVar", ""); + var setVariable = new SetVariable(variable, new Input("Hello World")); + var context = CreateMinimalActivityExecutionContext(setVariable); + + // Prepare the activity inputs in the context + PrepareActivityInputs(setVariable, context); + + // Act + await ExecuteActivityAsync(setVariable, context); + + // Assert + var result = variable.Get(context); + Assert.Equal("Hello World", result); + } + + /// + /// 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 like SetVariable. + /// + private static ActivityExecutionContext CreateMinimalActivityExecutionContext(IActivity activity) + { + // Create a minimal service provider with just the required services + var services = new ServiceCollection(); + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(_ => Substitute.For()); + services.AddSingleton(); + + // Mock the complex dependencies to avoid deep dependency chains + services.AddSingleton(_ => Substitute.For()); + + // Set up the activity registry lookup service to return proper descriptors for SetVariable activities + 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 = "Primitives", + 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()); + services.AddSingleton(_ => Substitute.For()); + + var serviceProvider = services.BuildServiceProvider(); + + // Create a minimal workflow + activity.Id = "test-workflow-activity"; // Ensure the activity has an ID + + 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", + CancellationToken.None + ).GetAwaiter().GetResult(); + + // Create ActivityExecutionContext for the actual activity we want to test + var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContextAsync(activity) + .GetAwaiter().GetResult(); + + return activityExecutionContext; + } + + /// + /// Helper method to execute an activity using reflection to access the protected ExecuteAsync method. + /// This enables testing activities in isolation without requiring the full workflow engine. + /// + private static async Task ExecuteActivityAsync(IActivity activity, ActivityExecutionContext context) + { + try + { + await context.EvaluateInputPropertiesAsync(); + await activity.ExecuteAsync(context); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + /// + /// Prepares activity inputs by declaring their memory blocks in the execution context. + /// This simulates what the workflow engine does to make input values available to activities. + /// Call this method before executing an activity to ensure its inputs are properly set up. + /// + private static void PrepareActivityInputs(IActivity activity, ActivityExecutionContext context) + { + Console.WriteLine($"Preparing inputs for activity: {activity.GetType().Name}"); + + var properties = activity.GetType().GetProperties(); + + foreach (var property in properties) + { + // Look for Input properties + if (property.PropertyType.IsGenericType && + property.PropertyType.GetGenericTypeDefinition() == typeof(Input<>)) + { + Console.WriteLine($"Found Input property: {property.Name}"); + + var inputValue = property.GetValue(activity); + if (inputValue != null) + { + // Get the MemoryBlockReference from the Input + var memoryBlockRefProperty = inputValue.GetType().GetProperty("MemoryBlockReference"); + if (memoryBlockRefProperty?.GetValue(inputValue) is MemoryBlockReference memoryBlockRef) + { + Console.WriteLine($"MemoryBlockReference ID: {memoryBlockRef.Id}"); + + // Get the expression from the Input + var expressionProperty = inputValue.GetType().GetProperty("Expression"); + var expression = expressionProperty?.GetValue(inputValue); + + if (expression != null) + { + Console.WriteLine($"Expression type: {expression.GetType().Name}"); + + // For Literal expressions, manually create and register the memory block + if (expression.GetType().IsGenericType && + expression.GetType().GetGenericTypeDefinition() == typeof(Literal<>)) + { + var valueProperty = expression.GetType().GetProperty("Value"); + var literalValue = valueProperty?.GetValue(expression); + + Console.WriteLine($"Literal value: {literalValue}"); + + // Create a memory block with the literal value directly + var memoryBlock = new MemoryBlock(literalValue); + + // Register it in the memory system using the memory block reference ID + context.ExpressionExecutionContext.Memory.Blocks[memoryBlockRef.Id] = memoryBlock; + + Console.WriteLine($"Registered memory block with ID: {memoryBlockRef.Id}"); + Console.WriteLine($"Total blocks in memory: {context.ExpressionExecutionContext.Memory.Blocks.Count}"); + + // Also try to register in workflow execution context memory if different + if (context.WorkflowExecutionContext.ExpressionExecutionContext.Memory != context.ExpressionExecutionContext.Memory) + { + context.WorkflowExecutionContext.ExpressionExecutionContext.Memory.Blocks[memoryBlockRef.Id] = memoryBlock; + Console.WriteLine("Also registered in workflow execution context memory"); + } + } + } + } + } + } + } + + // List all memory blocks for debugging + Console.WriteLine("All memory blocks:"); + foreach (var block in context.ExpressionExecutionContext.Memory.Blocks) + { + Console.WriteLine($" {block.Key} -> {block.Value.Value}"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Activities.UnitTests/Usings.cs b/test/unit/Elsa.Activities.UnitTests/Usings.cs new file mode 100644 index 000000000..b45935d97 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Usings.cs @@ -0,0 +1,3 @@ +global using Elsa.Workflows.Activities; +global using Elsa.Workflows.Memory; +global using Elsa.Workflows.Models; \ No newline at end of file From 5bd037928c426a8739b841337959ba8d882a1df2 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Wed, 8 Oct 2025 14:38:19 +0200 Subject: [PATCH 02/11] Helper for isolated activity testing + few SetVariable tests --- .../Elsa.Activities.UnitTests.csproj | 1 + .../Helpers/ActivityTestHelper.cs | 174 ++++++++++++++++ .../Primitives/SetVariableTests.cs | 196 +----------------- 3 files changed, 183 insertions(+), 188 deletions(-) create mode 100644 test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs diff --git a/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj index 3c3dec1fb..3017b8a33 100644 --- a/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj +++ b/test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj @@ -2,6 +2,7 @@ + diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs new file mode 100644 index 000000000..d5cf222fd --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -0,0 +1,174 @@ +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 + /// Task representing the async 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); + } + } +} diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index db4b90d02..e5ccb3b13 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -1,33 +1,19 @@ -using Elsa.Common; -using Elsa.Expressions.Contracts; -using Elsa.Expressions.Models; -using Elsa.Extensions; -using Elsa.Mediator.Contracts; +using Elsa.Activities.UnitTests.Helpers; using Elsa.Workflows; -using Elsa.Workflows.Activities; -using Elsa.Workflows.CommitStates; -using Elsa.Workflows.Memory; -using Elsa.Workflows.Models; -using Microsoft.Extensions.DependencyInjection; -using NSubstitute; namespace Elsa.Activities.UnitTests.Primitives; public class SetVariableTests { [Fact] - public async Task Should_Set_Variable() + public async Task Should_Set_Variable() { // Arrange - var variable = new Variable("myVar", 0); - var setVariable = new SetVariable(variable, new Input(42)); - var context = CreateMinimalActivityExecutionContext(setVariable); - - // Prepare the activity inputs in the context - //PrepareActivityInputs(setVariable, context); + var variable = new Variable("myVar", 0, "myVar"); + var setVariable = new SetVariable(variable, new Input(42, "inputId")); // Act - await ExecuteActivityAsync(setVariable, context); + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); // Assert var result = variable.Get(context); @@ -38,180 +24,14 @@ public class SetVariableTests public async Task Should_Set_Variable_From_Expression() { // Arrange - var variable = new Variable("myStringVar", ""); - var setVariable = new SetVariable(variable, new Input("Hello World")); - var context = CreateMinimalActivityExecutionContext(setVariable); - - // Prepare the activity inputs in the context - PrepareActivityInputs(setVariable, context); + var variable = new Variable("myStringVar", "", "myStringVar"); + var setVariable = new SetVariable(variable, new Input("Hello World", "inputId")); // Act - await ExecuteActivityAsync(setVariable, context); + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); // Assert var result = variable.Get(context); Assert.Equal("Hello World", result); } - - /// - /// 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 like SetVariable. - /// - private static ActivityExecutionContext CreateMinimalActivityExecutionContext(IActivity activity) - { - // Create a minimal service provider with just the required services - var services = new ServiceCollection(); - services.AddSingleton(_ => Substitute.For()); - services.AddSingleton(_ => Substitute.For()); - services.AddSingleton(); - - // Mock the complex dependencies to avoid deep dependency chains - services.AddSingleton(_ => Substitute.For()); - - // Set up the activity registry lookup service to return proper descriptors for SetVariable activities - 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 = "Primitives", - 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()); - services.AddSingleton(_ => Substitute.For()); - - var serviceProvider = services.BuildServiceProvider(); - - // Create a minimal workflow - activity.Id = "test-workflow-activity"; // Ensure the activity has an ID - - 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", - CancellationToken.None - ).GetAwaiter().GetResult(); - - // Create ActivityExecutionContext for the actual activity we want to test - var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContextAsync(activity) - .GetAwaiter().GetResult(); - - return activityExecutionContext; - } - - /// - /// Helper method to execute an activity using reflection to access the protected ExecuteAsync method. - /// This enables testing activities in isolation without requiring the full workflow engine. - /// - private static async Task ExecuteActivityAsync(IActivity activity, ActivityExecutionContext context) - { - try - { - await context.EvaluateInputPropertiesAsync(); - await activity.ExecuteAsync(context); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - - /// - /// Prepares activity inputs by declaring their memory blocks in the execution context. - /// This simulates what the workflow engine does to make input values available to activities. - /// Call this method before executing an activity to ensure its inputs are properly set up. - /// - private static void PrepareActivityInputs(IActivity activity, ActivityExecutionContext context) - { - Console.WriteLine($"Preparing inputs for activity: {activity.GetType().Name}"); - - var properties = activity.GetType().GetProperties(); - - foreach (var property in properties) - { - // Look for Input properties - if (property.PropertyType.IsGenericType && - property.PropertyType.GetGenericTypeDefinition() == typeof(Input<>)) - { - Console.WriteLine($"Found Input property: {property.Name}"); - - var inputValue = property.GetValue(activity); - if (inputValue != null) - { - // Get the MemoryBlockReference from the Input - var memoryBlockRefProperty = inputValue.GetType().GetProperty("MemoryBlockReference"); - if (memoryBlockRefProperty?.GetValue(inputValue) is MemoryBlockReference memoryBlockRef) - { - Console.WriteLine($"MemoryBlockReference ID: {memoryBlockRef.Id}"); - - // Get the expression from the Input - var expressionProperty = inputValue.GetType().GetProperty("Expression"); - var expression = expressionProperty?.GetValue(inputValue); - - if (expression != null) - { - Console.WriteLine($"Expression type: {expression.GetType().Name}"); - - // For Literal expressions, manually create and register the memory block - if (expression.GetType().IsGenericType && - expression.GetType().GetGenericTypeDefinition() == typeof(Literal<>)) - { - var valueProperty = expression.GetType().GetProperty("Value"); - var literalValue = valueProperty?.GetValue(expression); - - Console.WriteLine($"Literal value: {literalValue}"); - - // Create a memory block with the literal value directly - var memoryBlock = new MemoryBlock(literalValue); - - // Register it in the memory system using the memory block reference ID - context.ExpressionExecutionContext.Memory.Blocks[memoryBlockRef.Id] = memoryBlock; - - Console.WriteLine($"Registered memory block with ID: {memoryBlockRef.Id}"); - Console.WriteLine($"Total blocks in memory: {context.ExpressionExecutionContext.Memory.Blocks.Count}"); - - // Also try to register in workflow execution context memory if different - if (context.WorkflowExecutionContext.ExpressionExecutionContext.Memory != context.ExpressionExecutionContext.Memory) - { - context.WorkflowExecutionContext.ExpressionExecutionContext.Memory.Blocks[memoryBlockRef.Id] = memoryBlock; - Console.WriteLine("Also registered in workflow execution context memory"); - } - } - } - } - } - } - } - - // List all memory blocks for debugging - Console.WriteLine("All memory blocks:"); - foreach (var block in context.ExpressionExecutionContext.Memory.Blocks) - { - Console.WriteLine($" {block.Key} -> {block.Value.Value}"); - } - } } \ No newline at end of file From e6df8ea954edb16ce1cbd7dcdad4d95b432d02a7 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Wed, 8 Oct 2025 14:59:59 +0200 Subject: [PATCH 03/11] More unit tests for set variable --- .../Helpers/ActivityTestHelper.cs | 3 +- .../Primitives/SetVariableTests.cs | 422 +++++++++++++++++- 2 files changed, 422 insertions(+), 3 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs index d5cf222fd..9b01d48f0 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -25,7 +25,7 @@ public static class ActivityTestHelper /// evaluating inputs, and executing the activity. /// /// The activity to execute - /// Task representing the async execution + /// The ActivityExecutionContext used for execution public static async Task ExecuteActivityAsync(IActivity activity) { var context = CreateMinimalActivityExecutionContext(activity, out var serviceProvider); @@ -35,6 +35,7 @@ public static class ActivityTestHelper await SetupInputValuesInMemoryAsync(activity, context, serviceProvider); await context.EvaluateInputPropertiesAsync(); await activity.ExecuteAsync(context); + return context; } diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index e5ccb3b13..f94ba1812 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -1,12 +1,14 @@ using Elsa.Activities.UnitTests.Helpers; using Elsa.Workflows; +using Elsa.Expressions.Models; +using System.Text.Json; namespace Elsa.Activities.UnitTests.Primitives; public class SetVariableTests { [Fact] - public async Task Should_Set_Variable() + public async Task Should_Set_Variable_Integer() { // Arrange var variable = new Variable("myVar", 0, "myVar"); @@ -21,7 +23,7 @@ public class SetVariableTests } [Fact] - public async Task Should_Set_Variable_From_Expression() + public async Task Should_Set_Variable_String() { // Arrange var variable = new Variable("myStringVar", "", "myStringVar"); @@ -34,4 +36,420 @@ public class SetVariableTests var result = variable.Get(context); Assert.Equal("Hello World", result); } + + [Fact] + public async Task Should_Overwrite_Existing_Variable() + { + // Arrange + var variable = new Variable("existingVar", 100, "inputId"); + var setVariable = new SetVariable(variable, new Input(200, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert - verify the original value was overwritten + var result = variable.Get(context); + Assert.Equal(200, result); + } + + [Fact] + public async Task Should_Assign_Null_Value() + { + // Arrange + var variable = new Variable("nullVar", "initial", "nullVar"); + var setVariable = new SetVariable(variable, new Input((string)null!, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Null(result); + } + + [Fact] + public async Task Should_Set_Variable_With_Special_Characters() + { + // Arrange + var variable = new Variable("special_var-123", "", "specialVar"); + const string specialValue = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?"; + var setVariable = new SetVariable(variable, new Input(specialValue, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(specialValue, result); + } + + [Fact] + public async Task Should_Serialize_Complex_Object() + { + // Arrange + var complexObject = new ComplexTestObject + { + Id = 123, + Name = "Test Object", + Properties = new Dictionary + { + {"prop1", "value1"}, + {"prop2", 456}, + {"prop3", true} + }, + Items = ["item1", "item2", "item3"] + }; + + var variable = new Variable("complexVar", null!, "complexVar"); + var setVariable = new SetVariable(variable, new Input(complexObject, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.NotNull(result); + Assert.Equal(complexObject.Id, result.Id); + Assert.Equal(complexObject.Name, result.Name); + Assert.Equal(complexObject.Properties.Count, result.Properties.Count); + Assert.Equal(complexObject.Items.Length, result.Items.Length); + } + + [Fact] + public async Task Should_Assign_Large_Payload() + { + // Arrange + var largePayload = new string('A', 1024 * 1024); + var variable = new Variable("largeVar", "", "largeVar"); + var setVariable = new SetVariable(variable, new Input(largePayload, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(largePayload.Length, result.Length); + Assert.Equal(largePayload, result); + } + + [Fact] + public async Task Should_Handle_Case_Sensitivity_On_Variable_Names() + { + // Arrange + var variable1 = new Variable("CaseSensitive", "value1", "var1"); + var variable2 = new Variable("casesensitive", "value2", "var2"); + + var setVariable1 = new SetVariable(variable1, new Input("updated1", "input1")); + var setVariable2 = new SetVariable(variable2, new Input("updated2", "input2")); + + // Act + var context1 = await ActivityTestHelper.ExecuteActivityAsync(setVariable1); + var context2 = await ActivityTestHelper.ExecuteActivityAsync(setVariable2); + + // Assert - Variables with different casing should be treated as separate + var result1 = variable1.Get(context1); + var result2 = variable2.Get(context2); + + Assert.Equal("updated1", result1); + Assert.Equal("updated2", result2); + } + + [Fact] + public async Task Should_Reassign_Variable_Multiple_Times() + { + // Arrange + var variable = new Variable("multiVar", 0, "multiVar"); + + // Act - Multiple assignments + var setVariable1 = new SetVariable(variable, new Input(10, "input1")); + var context1 = await ActivityTestHelper.ExecuteActivityAsync(setVariable1); + var result1 = variable.Get(context1); + + var setVariable2 = new SetVariable(variable, new Input(20, "input2")); + var context2 = await ActivityTestHelper.ExecuteActivityAsync(setVariable2); + var result2 = variable.Get(context2); + + var setVariable3 = new SetVariable(variable, new Input(30, "input3")); + var context3 = await ActivityTestHelper.ExecuteActivityAsync(setVariable3); + var result3 = variable.Get(context3); + + // Assert - Each execution should update the variable + Assert.Equal(10, result1); + Assert.Equal(20, result2); + Assert.Equal(30, result3); + } + + [Fact] + public async Task Should_Handle_Boolean_Values() + { + // Arrange + var variable = new Variable("boolVar", false, "boolVar"); + var setVariable = new SetVariable(variable, new Input(true, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.True(result); + } + + [Fact] + public async Task Should_Handle_DateTime_Values() + { + // Arrange + var testDate = new DateTime(2025, 10, 8, 14, 30, 0); + var variable = new Variable("dateVar", DateTime.MinValue, "dateVar"); + var setVariable = new SetVariable(variable, new Input(testDate, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(testDate, result); + } + + [Fact] + public async Task Should_Handle_Decimal_Values() + { + // Arrange + var decimalValue = 123.456789m; + var variable = new Variable("decimalVar", 0m, "decimalVar"); + var setVariable = new SetVariable(variable, new Input(decimalValue, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(decimalValue, result); + } + + [Fact] + public async Task Should_Handle_Array_Values() + { + // Arrange + var arrayValue = new[] { "item1", "item2", "item3" }; + var variable = new Variable("arrayVar", null!, "arrayVar"); + var setVariable = new SetVariable(variable, new Input(arrayValue, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.NotNull(result); + Assert.Equal(arrayValue.Length, result.Length); + Assert.Equal(arrayValue, result); + } + + [Fact] + public async Task Should_Handle_Dictionary_Values() + { + // Arrange + var dictionaryValue = new Dictionary + { + {"key1", "value1"}, + {"key2", 123}, + {"key3", true} + }; + var variable = new Variable>("dictVar", null!, "dictVar"); + var setVariable = new SetVariable>(variable, new Input>(dictionaryValue, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.NotNull(result); + Assert.Equal(dictionaryValue.Count, result.Count); + Assert.Equal(dictionaryValue["key1"], result["key1"]); + Assert.Equal(dictionaryValue["key2"], result["key2"]); + Assert.Equal(dictionaryValue["key3"], result["key3"]); + } + + [Fact] + public async Task Should_Evaluate_Variable_Name_Via_Expression() + { + // Arrange - Use an expression for the variable name + var dynamicVariableName = "dynamic_var_" + DateTime.Now.Ticks; + var variable = new Variable(dynamicVariableName, "", "dynamicVar"); + var setVariable = new SetVariable(variable, new Input("Dynamic Value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("Dynamic Value", result); + Assert.Equal(dynamicVariableName, variable.Name); + } + + [Fact] + public async Task Should_Evaluate_Value_Via_Expression() + { + // Arrange - Use a computed expression for the value + var computedValue = $"Computed at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + var variable = new Variable("expressionVar", "", "expressionVar"); + var setVariable = new SetVariable(variable, new Input(computedValue, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal(computedValue, result); + Assert.Contains("Computed at", result); + } + + [Fact] + public async Task Should_Handle_Expression_That_Returns_Null() + { + // Arrange - Expression that evaluates to null + var variable = new Variable("nullExpressionVar", "initial", "nullExpressionVar"); + var setVariable = new SetVariable(variable, new Input((string)null!, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Null(result); + } + + [Fact] + public async Task Should_Handle_Undefined_Expression_Result() + { + // Arrange - Test handling of undefined/default values + var variable = new Variable("undefinedVar", null, "undefinedVar"); + var setVariable = new SetVariable(variable, new Input((int?)null, "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Null(result); + } + + [Fact] + public async Task Should_Handle_Zero_Length_Variable_Name() + { + // Arrange + var variable = new Variable(string.Empty, "test", "emptyNameVar"); + var setVariable = new SetVariable(variable, new Input("value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("value", result); + } + + [Fact] + public async Task Should_Handle_Unicode_Variable_Names() + { + // Arrange - Test with Unicode characters in variable names + var unicodeVariableName = "变量名_متغیر_переменная_🚀"; + var variable = new Variable(unicodeVariableName, "", "unicodeVar"); + var setVariable = new SetVariable(variable, new Input("Unicode Value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("Unicode Value", result); + Assert.Equal(unicodeVariableName, variable.Name); + } + + [Fact] + public async Task Should_Handle_Extremely_Long_Variable_Name() + { + // Arrange - Test with very long variable name + var longVariableName = new string('a', 1000); + var variable = new Variable(longVariableName, "", "longNameVar"); + var setVariable = new SetVariable(variable, new Input("Long Name Value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("Long Name Value", result); + Assert.Equal(1000, variable.Name.Length); + } + + [Fact] + public async Task Should_Handle_Numeric_Variable_Names() + { + // Arrange - Test with numeric variable names + var numericVariableName = "12345"; + var variable = new Variable(numericVariableName, "", "numericVar"); + var setVariable = new SetVariable(variable, new Input("Numeric Name Value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("Numeric Name Value", result); + Assert.Equal(numericVariableName, variable.Name); + } + + [Fact] + public async Task Should_Handle_Variable_Name_With_Spaces() + { + // Arrange - Test with spaces in variable names + var spacedVariableName = "variable with spaces"; + var variable = new Variable(spacedVariableName, "", "spacedVar"); + var setVariable = new SetVariable(variable, new Input("Spaced Name Value", "inputId")); + + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + + // Assert + var result = variable.Get(context); + Assert.Equal("Spaced Name Value", result); + Assert.Equal(spacedVariableName, variable.Name); + } + + [Fact] + public async Task Should_Handle_Concurrent_Variable_Updates() + { + // Arrange - Test thread safety with concurrent updates + var variable = new Variable("concurrentVar", 0, "concurrentVar"); + var tasks = new List>(); + + // Act - Create multiple concurrent assignments + for (int i = 1; i <= 10; i++) + { + var value = i; + tasks.Add(Task.Run(async () => + { + var setVariable = new SetVariable(variable, new Input(value, $"input{value}")); + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); + return variable.Get(context); + })); + } + + var results = await Task.WhenAll(tasks); + + // Assert - All operations should complete successfully + Assert.Equal(10, results.Length); + Assert.All(results, result => Assert.True(result >= 1 && result <= 10)); + } +} + +/// +/// Complex test object for serialization testing +/// +public class ComplexTestObject +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public Dictionary Properties { get; set; } = new(); + public string[] Items { get; set; } = []; } \ No newline at end of file From 8eb2b11f6e4a6272bfa8da2eab8d6ca1fb71ab97 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 8 Oct 2025 20:23:25 +0200 Subject: [PATCH 04/11] Refactor `SetVariable` activity: add null safety checks, update variable property to nullable type, and enhance unit tests for edge cases. --- .../Activities/SetVariable.cs | 10 +- .../Services/ActivityRegistry.cs | 1 - .../Helpers/ActivityTestHelper.cs | 136 ++---- .../Primitives/SetVariableTests.cs | 440 +----------------- 4 files changed, 66 insertions(+), 521 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs index a12b35086..ba99e893d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs @@ -89,7 +89,7 @@ public class SetVariable : CodeActivity /// The variable to assign the value to. /// [Input(Description = "The variable to assign the value to.")] - public Variable Variable { get; set; } = null!; + public Variable? Variable { get; set; } /// /// The value to assign. @@ -101,9 +101,13 @@ public class SetVariable : CodeActivity protected override void Execute(ActivityExecutionContext context) { // Always refer to the variable by ID to ensure that the variable is resolved from the correct scope. - var variableId = Variable.Id; + var variableId = Variable?.Id; var variable = context.ExpressionExecutionContext.EnumerateVariablesInScope().FirstOrDefault(x => x.Id == variableId); + + if (variable == null) + throw new($"Variable '{variableId}' not found."); + var value = context.Get(Value); - variable?.Set(context, value); + variable.Set(context, value); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs index 282d07be3..f5b4482ca 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs @@ -57,7 +57,6 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable< var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken); - Add(activityDescriptor, _activityDescriptors, _manualActivityDescriptors); _manualActivityDescriptors.Add(activityDescriptor); } diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs index 9b01d48f0..1a2adedb4 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -7,6 +7,7 @@ using Elsa.Workflows; using Elsa.Workflows.CommitStates; using Elsa.Workflows.Management.Providers; using Elsa.Workflows.Management.Services; +using Elsa.Workflows.PortResolvers; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -26,16 +27,15 @@ public static class ActivityTestHelper /// /// The activity to execute /// The ActivityExecutionContext used for execution - public static async Task ExecuteActivityAsync(IActivity activity) + public static async Task ExecuteActivityAsync(IActivity activity, Action? configureServices = null) { - var context = CreateMinimalActivityExecutionContext(activity, out var serviceProvider); - + var context = await CreateMinimalActivityExecutionContext(activity, configureServices); + // 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; } @@ -44,82 +44,61 @@ public static class ActivityTestHelper /// 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) + private static async Task CreateMinimalActivityExecutionContext(IActivity activity, Action? configureServices) { // Create a minimal service provider with the required services for expression evaluation var services = new ServiceCollection(); - + // Add core services + services.AddLogging(); 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(); - + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // 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); - + // Call the configure services action if provided. + configureServices?.Invoke(services); + + 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 = WorkflowExecutionContext.CreateAsync( - serviceProvider, - workflowGraph, - $"test-instance-{Guid.NewGuid()}", + var workflowExecutionContext = await 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; + return await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); } /// @@ -133,7 +112,7 @@ public static class ActivityTestHelper .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); @@ -141,35 +120,4 @@ public static class ActivityTestHelper 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); - } - } -} +} \ No newline at end of file diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index f94ba1812..15d7bf0d8 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -1,455 +1,49 @@ using Elsa.Activities.UnitTests.Helpers; -using Elsa.Workflows; -using Elsa.Expressions.Models; -using System.Text.Json; namespace Elsa.Activities.UnitTests.Primitives; public class SetVariableTests { [Fact] - public async Task Should_Set_Variable_Integer() + public async Task Should_Set_Variable_Integer() { // Arrange + const int expected = 42; // The answer to life, the universe and everything. var variable = new Variable("myVar", 0, "myVar"); - var setVariable = new SetVariable(variable, new Input(42, "inputId")); - + var setVariable = new SetVariable(variable, new Input(expected)); + // Act var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - + // Assert var result = variable.Get(context); - Assert.Equal(42, result); + Assert.Equal(expected, result); } [Fact] - public async Task Should_Set_Variable_String() + public async Task Should_Not_Throw_When_Variable_Is_Null() { // Arrange - var variable = new Variable("myStringVar", "", "myStringVar"); - var setVariable = new SetVariable(variable, new Input("Hello World", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Hello World", result); + var setVariable = new SetVariable(null!, new Input("test value")); + + // Act & Assert + var exception = await Record.ExceptionAsync(async () => await ActivityTestHelper.ExecuteActivityAsync(setVariable)); + + Assert.NotNull(exception); } [Fact] - public async Task Should_Overwrite_Existing_Variable() + public async Task Should_Set_Variable_To_Null_Value() { // Arrange - var variable = new Variable("existingVar", 100, "inputId"); - var setVariable = new SetVariable(variable, new Input(200, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - verify the original value was overwritten - var result = variable.Get(context); - Assert.Equal(200, result); - } + var variable = new Variable("myVar", "initial value", "myVar"); + var setVariable = new SetVariable(variable, new Input(default(string))); - [Fact] - public async Task Should_Assign_Null_Value() - { - // Arrange - var variable = new Variable("nullVar", "initial", "nullVar"); - var setVariable = new SetVariable(variable, new Input((string)null!, "inputId")); - // Act var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - + // Assert var result = variable.Get(context); Assert.Null(result); } - - [Fact] - public async Task Should_Set_Variable_With_Special_Characters() - { - // Arrange - var variable = new Variable("special_var-123", "", "specialVar"); - const string specialValue = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?"; - var setVariable = new SetVariable(variable, new Input(specialValue, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal(specialValue, result); - } - - [Fact] - public async Task Should_Serialize_Complex_Object() - { - // Arrange - var complexObject = new ComplexTestObject - { - Id = 123, - Name = "Test Object", - Properties = new Dictionary - { - {"prop1", "value1"}, - {"prop2", 456}, - {"prop3", true} - }, - Items = ["item1", "item2", "item3"] - }; - - var variable = new Variable("complexVar", null!, "complexVar"); - var setVariable = new SetVariable(variable, new Input(complexObject, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.NotNull(result); - Assert.Equal(complexObject.Id, result.Id); - Assert.Equal(complexObject.Name, result.Name); - Assert.Equal(complexObject.Properties.Count, result.Properties.Count); - Assert.Equal(complexObject.Items.Length, result.Items.Length); - } - - [Fact] - public async Task Should_Assign_Large_Payload() - { - // Arrange - var largePayload = new string('A', 1024 * 1024); - var variable = new Variable("largeVar", "", "largeVar"); - var setVariable = new SetVariable(variable, new Input(largePayload, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal(largePayload.Length, result.Length); - Assert.Equal(largePayload, result); - } - - [Fact] - public async Task Should_Handle_Case_Sensitivity_On_Variable_Names() - { - // Arrange - var variable1 = new Variable("CaseSensitive", "value1", "var1"); - var variable2 = new Variable("casesensitive", "value2", "var2"); - - var setVariable1 = new SetVariable(variable1, new Input("updated1", "input1")); - var setVariable2 = new SetVariable(variable2, new Input("updated2", "input2")); - - // Act - var context1 = await ActivityTestHelper.ExecuteActivityAsync(setVariable1); - var context2 = await ActivityTestHelper.ExecuteActivityAsync(setVariable2); - - // Assert - Variables with different casing should be treated as separate - var result1 = variable1.Get(context1); - var result2 = variable2.Get(context2); - - Assert.Equal("updated1", result1); - Assert.Equal("updated2", result2); - } - - [Fact] - public async Task Should_Reassign_Variable_Multiple_Times() - { - // Arrange - var variable = new Variable("multiVar", 0, "multiVar"); - - // Act - Multiple assignments - var setVariable1 = new SetVariable(variable, new Input(10, "input1")); - var context1 = await ActivityTestHelper.ExecuteActivityAsync(setVariable1); - var result1 = variable.Get(context1); - - var setVariable2 = new SetVariable(variable, new Input(20, "input2")); - var context2 = await ActivityTestHelper.ExecuteActivityAsync(setVariable2); - var result2 = variable.Get(context2); - - var setVariable3 = new SetVariable(variable, new Input(30, "input3")); - var context3 = await ActivityTestHelper.ExecuteActivityAsync(setVariable3); - var result3 = variable.Get(context3); - - // Assert - Each execution should update the variable - Assert.Equal(10, result1); - Assert.Equal(20, result2); - Assert.Equal(30, result3); - } - - [Fact] - public async Task Should_Handle_Boolean_Values() - { - // Arrange - var variable = new Variable("boolVar", false, "boolVar"); - var setVariable = new SetVariable(variable, new Input(true, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.True(result); - } - - [Fact] - public async Task Should_Handle_DateTime_Values() - { - // Arrange - var testDate = new DateTime(2025, 10, 8, 14, 30, 0); - var variable = new Variable("dateVar", DateTime.MinValue, "dateVar"); - var setVariable = new SetVariable(variable, new Input(testDate, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal(testDate, result); - } - - [Fact] - public async Task Should_Handle_Decimal_Values() - { - // Arrange - var decimalValue = 123.456789m; - var variable = new Variable("decimalVar", 0m, "decimalVar"); - var setVariable = new SetVariable(variable, new Input(decimalValue, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal(decimalValue, result); - } - - [Fact] - public async Task Should_Handle_Array_Values() - { - // Arrange - var arrayValue = new[] { "item1", "item2", "item3" }; - var variable = new Variable("arrayVar", null!, "arrayVar"); - var setVariable = new SetVariable(variable, new Input(arrayValue, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.NotNull(result); - Assert.Equal(arrayValue.Length, result.Length); - Assert.Equal(arrayValue, result); - } - - [Fact] - public async Task Should_Handle_Dictionary_Values() - { - // Arrange - var dictionaryValue = new Dictionary - { - {"key1", "value1"}, - {"key2", 123}, - {"key3", true} - }; - var variable = new Variable>("dictVar", null!, "dictVar"); - var setVariable = new SetVariable>(variable, new Input>(dictionaryValue, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.NotNull(result); - Assert.Equal(dictionaryValue.Count, result.Count); - Assert.Equal(dictionaryValue["key1"], result["key1"]); - Assert.Equal(dictionaryValue["key2"], result["key2"]); - Assert.Equal(dictionaryValue["key3"], result["key3"]); - } - - [Fact] - public async Task Should_Evaluate_Variable_Name_Via_Expression() - { - // Arrange - Use an expression for the variable name - var dynamicVariableName = "dynamic_var_" + DateTime.Now.Ticks; - var variable = new Variable(dynamicVariableName, "", "dynamicVar"); - var setVariable = new SetVariable(variable, new Input("Dynamic Value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Dynamic Value", result); - Assert.Equal(dynamicVariableName, variable.Name); - } - - [Fact] - public async Task Should_Evaluate_Value_Via_Expression() - { - // Arrange - Use a computed expression for the value - var computedValue = $"Computed at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; - var variable = new Variable("expressionVar", "", "expressionVar"); - var setVariable = new SetVariable(variable, new Input(computedValue, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal(computedValue, result); - Assert.Contains("Computed at", result); - } - - [Fact] - public async Task Should_Handle_Expression_That_Returns_Null() - { - // Arrange - Expression that evaluates to null - var variable = new Variable("nullExpressionVar", "initial", "nullExpressionVar"); - var setVariable = new SetVariable(variable, new Input((string)null!, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Null(result); - } - - [Fact] - public async Task Should_Handle_Undefined_Expression_Result() - { - // Arrange - Test handling of undefined/default values - var variable = new Variable("undefinedVar", null, "undefinedVar"); - var setVariable = new SetVariable(variable, new Input((int?)null, "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Null(result); - } - - [Fact] - public async Task Should_Handle_Zero_Length_Variable_Name() - { - // Arrange - var variable = new Variable(string.Empty, "test", "emptyNameVar"); - var setVariable = new SetVariable(variable, new Input("value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("value", result); - } - - [Fact] - public async Task Should_Handle_Unicode_Variable_Names() - { - // Arrange - Test with Unicode characters in variable names - var unicodeVariableName = "变量名_متغیر_переменная_🚀"; - var variable = new Variable(unicodeVariableName, "", "unicodeVar"); - var setVariable = new SetVariable(variable, new Input("Unicode Value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Unicode Value", result); - Assert.Equal(unicodeVariableName, variable.Name); - } - - [Fact] - public async Task Should_Handle_Extremely_Long_Variable_Name() - { - // Arrange - Test with very long variable name - var longVariableName = new string('a', 1000); - var variable = new Variable(longVariableName, "", "longNameVar"); - var setVariable = new SetVariable(variable, new Input("Long Name Value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Long Name Value", result); - Assert.Equal(1000, variable.Name.Length); - } - - [Fact] - public async Task Should_Handle_Numeric_Variable_Names() - { - // Arrange - Test with numeric variable names - var numericVariableName = "12345"; - var variable = new Variable(numericVariableName, "", "numericVar"); - var setVariable = new SetVariable(variable, new Input("Numeric Name Value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Numeric Name Value", result); - Assert.Equal(numericVariableName, variable.Name); - } - - [Fact] - public async Task Should_Handle_Variable_Name_With_Spaces() - { - // Arrange - Test with spaces in variable names - var spacedVariableName = "variable with spaces"; - var variable = new Variable(spacedVariableName, "", "spacedVar"); - var setVariable = new SetVariable(variable, new Input("Spaced Name Value", "inputId")); - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - - // Assert - var result = variable.Get(context); - Assert.Equal("Spaced Name Value", result); - Assert.Equal(spacedVariableName, variable.Name); - } - - [Fact] - public async Task Should_Handle_Concurrent_Variable_Updates() - { - // Arrange - Test thread safety with concurrent updates - var variable = new Variable("concurrentVar", 0, "concurrentVar"); - var tasks = new List>(); - - // Act - Create multiple concurrent assignments - for (int i = 1; i <= 10; i++) - { - var value = i; - tasks.Add(Task.Run(async () => - { - var setVariable = new SetVariable(variable, new Input(value, $"input{value}")); - var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); - return variable.Get(context); - })); - } - - var results = await Task.WhenAll(tasks); - - // Assert - All operations should complete successfully - Assert.Equal(10, results.Length); - Assert.All(results, result => Assert.True(result >= 1 && result <= 10)); - } -} - -/// -/// Complex test object for serialization testing -/// -public class ComplexTestObject -{ - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - public Dictionary Properties { get; set; } = new(); - public string[] Items { get; set; } = []; } \ No newline at end of file From b3c1defd3dab78d7c5f8e4d38b5c8244c8519aaf Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 8 Oct 2025 20:27:18 +0200 Subject: [PATCH 05/11] Fix incorrect test method name to reflect expected throwing behavior when variable is null in `SetVariableTests`. --- .../Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index 15d7bf0d8..0adc16ffa 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -21,7 +21,7 @@ public class SetVariableTests } [Fact] - public async Task Should_Not_Throw_When_Variable_Is_Null() + public async Task Should_Throw_When_Variable_Is_Null() { // Arrange var setVariable = new SetVariable(null!, new Input("test value")); From abfaad85cbc509c62d8141f2bbc2a10797945034 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Thu, 9 Oct 2025 09:39:07 +0200 Subject: [PATCH 06/11] Update test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs index 1a2adedb4..68d47361e 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -59,7 +59,6 @@ public static class ActivityTestHelper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); From c02484fd4debff3fd5282d215daec8dbd80694ec Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Thu, 9 Oct 2025 09:40:01 +0200 Subject: [PATCH 07/11] Update src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs index ba99e893d..689e847e6 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs @@ -105,7 +105,7 @@ public class SetVariable : CodeActivity var variable = context.ExpressionExecutionContext.EnumerateVariablesInScope().FirstOrDefault(x => x.Id == variableId); if (variable == null) - throw new($"Variable '{variableId}' not found."); + throw new InvalidOperationException($"Variable '{variableId}' not found."); var value = context.Get(Value); variable.Set(context, value); From c7df5619e58518eb684e8c0c21d8fa601c5d519e Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Thu, 9 Oct 2025 09:40:36 +0200 Subject: [PATCH 08/11] Update test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index 0adc16ffa..7b94781ef 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -37,7 +37,7 @@ public class SetVariableTests { // Arrange var variable = new Variable("myVar", "initial value", "myVar"); - var setVariable = new SetVariable(variable, new Input(default(string))); + var setVariable = new SetVariable(variable, new Input(null)); // Act var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); From 9ea3f49e0fbebd6483f02cb23a7da7d25a319b44 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 9 Oct 2025 09:41:27 +0200 Subject: [PATCH 09/11] Small improvement for generic Variable type evaluation --- .../Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs index 1a2adedb4..d01f77a29 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -26,6 +26,7 @@ public static class ActivityTestHelper /// evaluating inputs, and executing the activity. /// /// The activity to execute + /// /// The ActivityExecutionContext used for execution public static async Task ExecuteActivityAsync(IActivity activity, Action? configureServices = null) { @@ -110,7 +111,7 @@ public static class ActivityTestHelper var activityType = activity.GetType(); var variableProperties = activityType.GetProperties() .Where(p => p.PropertyType.IsGenericType && - p.PropertyType.BaseType == typeof(Variable)) + p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>)) .ToList(); foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!)) From 19b5ab94dd760f97b1c5eaf67408b60c8537d122 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 9 Oct 2025 09:59:04 +0200 Subject: [PATCH 10/11] Workaround for using correct overoad - build fix --- .../Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs index 7b94781ef..2f5c7ec5d 100644 --- a/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/SetVariableTests.cs @@ -37,7 +37,7 @@ public class SetVariableTests { // Arrange var variable = new Variable("myVar", "initial value", "myVar"); - var setVariable = new SetVariable(variable, new Input(null)); + var setVariable = new SetVariable(variable, new Input((string?)null)); // Act var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); From 5b014c7566587ae5db61fcaca6472582092a3352 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 9 Oct 2025 13:31:06 +0200 Subject: [PATCH 11/11] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs index a17ab01f9..b8f623ed6 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs +++ b/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs @@ -26,7 +26,7 @@ public static class ActivityTestHelper /// evaluating inputs, and executing the activity. /// /// The activity to execute - /// + /// An optional action to configure the service collection for dependency injection during activity execution. /// The ActivityExecutionContext used for execution public static async Task ExecuteActivityAsync(IActivity activity, Action? configureServices = null) {