(WIP) Initial context helper + provisional unit tests for setvariable
This commit is contained in:
parent
fcdb6f5499
commit
4a26819ed7
9
Elsa.sln
9
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}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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<int>("myVar", 0);
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(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<string>("myStringVar", "");
|
||||
var setVariable = new SetVariable<string>(variable, new Input<string>("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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static ActivityExecutionContext CreateMinimalActivityExecutionContext(IActivity activity)
|
||||
{
|
||||
// Create a minimal service provider with just the required services
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<ISystemClock>(_ => Substitute.For<ISystemClock>());
|
||||
services.AddSingleton<INotificationSender>(_ => Substitute.For<INotificationSender>());
|
||||
services.AddSingleton<IActivityVisitor, ActivityVisitor>();
|
||||
|
||||
// Mock the complex dependencies to avoid deep dependency chains
|
||||
services.AddSingleton<IActivityRegistry>(_ => Substitute.For<IActivityRegistry>());
|
||||
|
||||
// Set up the activity registry lookup service to return proper descriptors for SetVariable activities
|
||||
var activityRegistryLookup = Substitute.For<IActivityRegistryLookupService>();
|
||||
activityRegistryLookup.FindAsync(Arg.Any<string>(), Arg.Any<int>()).Returns(callInfo =>
|
||||
{
|
||||
var activityType = callInfo.ArgAt<string>(0);
|
||||
return Task.FromResult<ActivityDescriptor?>(new ActivityDescriptor
|
||||
{
|
||||
TypeName = activityType,
|
||||
Kind = ActivityKind.Action,
|
||||
Category = "Primitives",
|
||||
Description = "Test activity for unit testing",
|
||||
Version = 1
|
||||
});
|
||||
});
|
||||
services.AddSingleton<IActivityRegistryLookupService>(_ => activityRegistryLookup);
|
||||
|
||||
services.AddSingleton<IIdentityGraphService>(_ => Substitute.For<IIdentityGraphService>());
|
||||
services.AddSingleton<IWorkflowGraphBuilder>(_ => Substitute.For<IWorkflowGraphBuilder>());
|
||||
services.AddSingleton<IHasher>(_ => Substitute.For<IHasher>());
|
||||
services.AddSingleton<ICommitStateHandler>(_ => Substitute.For<ICommitStateHandler>());
|
||||
services.AddSingleton<IActivitySchedulerFactory>(_ => Substitute.For<IActivitySchedulerFactory>());
|
||||
services.AddSingleton<IIdentityGenerator>(_ => Substitute.For<IIdentityGenerator>());
|
||||
|
||||
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<ActivityNode> { 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static async Task ExecuteActivityAsync(IActivity activity, ActivityExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
await activity.ExecuteAsync(context);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<T> 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<T>
|
||||
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<T>
|
||||
var expressionProperty = inputValue.GetType().GetProperty("Expression");
|
||||
var expression = expressionProperty?.GetValue(inputValue);
|
||||
|
||||
if (expression != null)
|
||||
{
|
||||
Console.WriteLine($"Expression type: {expression.GetType().Name}");
|
||||
|
||||
// For Literal<T> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
3
test/unit/Elsa.Activities.UnitTests/Usings.cs
Normal file
3
test/unit/Elsa.Activities.UnitTests/Usings.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
global using Elsa.Workflows.Activities;
|
||||
global using Elsa.Workflows.Memory;
|
||||
global using Elsa.Workflows.Models;
|
||||
Loading…
Reference in a new issue