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