Add unit and integration tests for Container activity, covering behavior such as variable scoping, child activity execution, and mixed variable types. (#7102)
* Add unit and integration tests for `Container` activity, covering behavior such as variable scoping, child activity execution, and mixed variable types. * Refactor `RunWorkflowAndCaptureOutput` method in `ContainerTests` for better code organization * Extract `TestContainer` to `Elsa.Testing.Shared.Activities` for reuse across test projects.
This commit is contained in:
parent
faebea76a0
commit
e936d636ee
38
src/common/Elsa.Testing.Shared/Activities/TestContainer.cs
Normal file
38
src/common/Elsa.Testing.Shared/Activities/TestContainer.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Testing.Shared.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// Concrete implementation of Container for testing that schedules children sequentially.
|
||||
/// </summary>
|
||||
public class TestContainer : Container
|
||||
{
|
||||
private const string CurrentIndexProperty = "CurrentIndex";
|
||||
|
||||
protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
|
||||
{
|
||||
await HandleItemAsync(context);
|
||||
}
|
||||
|
||||
private async ValueTask HandleItemAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var currentIndex = context.GetProperty<int>(CurrentIndexProperty);
|
||||
var childActivities = Activities.ToList();
|
||||
|
||||
if (currentIndex >= childActivities.Count)
|
||||
{
|
||||
await context.CompleteActivityAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var nextActivity = childActivities.ElementAt(currentIndex);
|
||||
await context.ScheduleActivityAsync(nextActivity, OnChildCompleted);
|
||||
context.UpdateProperty<int>(CurrentIndexProperty, x => x + 1);
|
||||
}
|
||||
|
||||
private async ValueTask OnChildCompleted(ActivityCompletedContext context)
|
||||
{
|
||||
await HandleItemAsync(context.TargetContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Models;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Container;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the <see cref="Container"/> activity.
|
||||
/// </summary>
|
||||
public class ContainerTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
|
||||
|
||||
[Fact(DisplayName = "Container executes child activities in order")]
|
||||
public async Task Container_ExecutesChildActivitiesInOrder()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new SimpleContainerWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Equal(new[] { "Activity 1", "Activity 2", "Activity 3" }, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with variables makes them available to child activities")]
|
||||
public async Task Container_WithVariables_MakesThemAvailableToChildren()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new ContainerWithVariablesWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Contains("Counter: 0", lines[0]);
|
||||
Assert.Contains("Counter: 1", lines[1]);
|
||||
Assert.Contains("Counter: 2", lines[2]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with nested containers scopes variables correctly")]
|
||||
public async Task Container_WithNestedContainers_ScopesVariablesCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new NestedContainersWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Contains("Outer: 10", lines[0]);
|
||||
Assert.Contains("Inner: 20", lines[1]);
|
||||
Assert.Contains("Outer again: 10", lines[2]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with unnamed variables auto-names them")]
|
||||
public async Task Container_WithUnnamedVariables_AutoNamesThem()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, _) = await RunWorkflowAndCaptureOutput(new ContainerWithUnnamedVariablesWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
// The workflow should complete successfully even with unnamed variables
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with no activities completes successfully")]
|
||||
public async Task Container_WithNoActivities_CompletesSuccessfully()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new EmptyContainerWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Empty(lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with multiple variable types handles them correctly")]
|
||||
public async Task Container_WithMultipleVariableTypes_HandlesThemCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new ContainerWithMixedVariableTypesWorkflow());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Contains("Int: 42", lines[0]);
|
||||
Assert.Contains("String: Hello", lines[1]);
|
||||
Assert.Contains("Bool: True", lines[2]);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Container handles different numbers of child activities")]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public async Task Container_HandlesDifferentNumbersOfChildActivities(int activityCount)
|
||||
{
|
||||
// Arrange & Act
|
||||
var (result, lines) = await RunWorkflowAndCaptureOutput(new DynamicContainerWorkflow(activityCount));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
Assert.Equal(activityCount, lines.Count);
|
||||
for (var i = 0; i < activityCount; i++)
|
||||
{
|
||||
Assert.Equal($"Activity {i + 1}", lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(RunWorkflowResult Result, List<string> Lines)> RunWorkflowAndCaptureOutput(IWorkflow workflow)
|
||||
{
|
||||
var result = await _fixture.RunWorkflowAsync(workflow);
|
||||
var lines = _fixture.CapturingTextWriter.Lines.ToList();
|
||||
return (result, lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
using Elsa.Testing.Shared.Activities;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.Container;
|
||||
|
||||
/// <summary>
|
||||
/// Simple workflow with a container that executes activities sequentially.
|
||||
/// </summary>
|
||||
public class SimpleContainerWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
workflow.Root = new TestContainer
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Activity 1"),
|
||||
new WriteLine("Activity 2"),
|
||||
new WriteLine("Activity 3")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow demonstrating container with variables.
|
||||
/// </summary>
|
||||
public class ContainerWithVariablesWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var counter = new Variable<int>("Counter", 0);
|
||||
|
||||
workflow.Root = new TestContainer
|
||||
{
|
||||
Variables = { counter },
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Counter: {counter.Get(context)}"),
|
||||
new SetVariable<int> { Variable = counter, Value = new(1) },
|
||||
new WriteLine(context => $"Counter: {counter.Get(context)}"),
|
||||
new SetVariable<int> { Variable = counter, Value = new(2) },
|
||||
new WriteLine(context => $"Counter: {counter.Get(context)}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow with nested containers to test variable scoping.
|
||||
/// </summary>
|
||||
public class NestedContainersWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var outerVar = new Variable<int>("OuterVar", 10);
|
||||
var innerVar = new Variable<int>("InnerVar", 20);
|
||||
|
||||
workflow.Root = new TestContainer
|
||||
{
|
||||
Variables = { outerVar },
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Outer: {outerVar.Get(context)}"),
|
||||
new TestContainer
|
||||
{
|
||||
Variables = { innerVar },
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Inner: {innerVar.Get(context)}")
|
||||
}
|
||||
},
|
||||
new WriteLine(context => $"Outer again: {outerVar.Get(context)}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow with unnamed variables to test auto-naming.
|
||||
/// </summary>
|
||||
public class ContainerWithUnnamedVariablesWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
workflow.Root = new TestContainer
|
||||
{
|
||||
Variables =
|
||||
{
|
||||
new Variable<int>(),
|
||||
new Variable<string>(),
|
||||
new Variable<bool>()
|
||||
},
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Unnamed variables workflow")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow with an empty container (no activities).
|
||||
/// </summary>
|
||||
public class EmptyContainerWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
workflow.Root = new TestContainer();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow with mixed variable types.
|
||||
/// </summary>
|
||||
public class ContainerWithMixedVariableTypesWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var intVar = new Variable<int>("IntVar", 42);
|
||||
var stringVar = new Variable<string>("StringVar", "Hello");
|
||||
var boolVar = new Variable<bool>("BoolVar", true);
|
||||
|
||||
workflow.Root = new TestContainer
|
||||
{
|
||||
Variables = { intVar, stringVar, boolVar },
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Int: {intVar.Get(context)}"),
|
||||
new WriteLine(context => $"String: {stringVar.Get(context)}"),
|
||||
new WriteLine(context => $"Bool: {boolVar.Get(context)}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow with a dynamic number of activities.
|
||||
/// </summary>
|
||||
public class DynamicContainerWorkflow(int activityCount) : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder workflow)
|
||||
{
|
||||
var container = new TestContainer();
|
||||
|
||||
for (var i = 1; i <= activityCount; i++)
|
||||
{
|
||||
container.Activities.Add(new WriteLine($"Activity {i}"));
|
||||
}
|
||||
|
||||
workflow.Root = container;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Testing.Shared.Activities;
|
||||
using Elsa.Workflows;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Container"/> activity focusing on ExecuteAsync behavior.
|
||||
/// </summary>
|
||||
public class ContainerTests
|
||||
{
|
||||
[Theory(DisplayName = "Container schedules child activities")]
|
||||
[InlineData(1)]
|
||||
[InlineData(3)]
|
||||
[InlineData(5)]
|
||||
public async Task Container_SchedulesChildActivities(int activityCount)
|
||||
{
|
||||
// Arrange
|
||||
var activities = CreateActivities(activityCount);
|
||||
var container = CreateContainerWithActivities(activities);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert - First activity should be scheduled (sequential scheduling)
|
||||
Assert.True(context.HasScheduledActivity(activities[0]));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with no activities completes without scheduling")]
|
||||
public async Task Container_WithNoActivities_CompletesWithoutScheduling()
|
||||
{
|
||||
// Arrange
|
||||
var container = new TestContainer();
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Empty(scheduledActivities);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container declares variables in memory")]
|
||||
public async Task Container_DeclaresVariablesInMemory()
|
||||
{
|
||||
// Arrange
|
||||
var variable1 = new Variable<int>("Counter", 10);
|
||||
var variable2 = new Variable<string>("Name", "Test");
|
||||
var container = CreateContainerWithVariables(variable1, variable2);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
var memory = context.ExpressionExecutionContext.Memory;
|
||||
Assert.True(memory.HasBlock(variable1.Id));
|
||||
Assert.True(memory.HasBlock(variable2.Id));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container auto-names unnamed variables")]
|
||||
public async Task Container_AutoNamesUnnamedVariables()
|
||||
{
|
||||
// Arrange
|
||||
var var1 = new Variable<int>();
|
||||
var var2 = new Variable<string>();
|
||||
var var3 = new Variable<bool>();
|
||||
var container = CreateContainerWithVariables(var1, var2, var3);
|
||||
|
||||
// Act
|
||||
await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Variable1", var1.Name);
|
||||
Assert.Equal("Variable2", var2.Name);
|
||||
Assert.Equal("Variable3", var3.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container preserves named variables")]
|
||||
public async Task Container_PreservesNamedVariables()
|
||||
{
|
||||
// Arrange
|
||||
var namedVar = new Variable<string>("MyVariable", "value");
|
||||
var unnamedVar = new Variable<int>();
|
||||
var container = CreateContainerWithVariables(namedVar, unnamedVar);
|
||||
|
||||
// Act
|
||||
await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("MyVariable", namedVar.Name);
|
||||
Assert.Equal("Variable1", unnamedVar.Name);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Container handles multiple unnamed variables correctly")]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public async Task Container_HandlesMultipleUnnamedVariables(int variableCount)
|
||||
{
|
||||
// Arrange
|
||||
var variables = CreateUnnamedVariables(variableCount);
|
||||
var container = CreateContainerWithVariables(variables.Cast<Variable>().ToArray());
|
||||
|
||||
// Act
|
||||
await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
for (var i = 0; i < variableCount; i++)
|
||||
{
|
||||
Assert.Equal($"Variable{i + 1}", variables[i].Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with mixed named and unnamed variables")]
|
||||
public async Task Container_WithMixedNamedAndUnnamedVariables()
|
||||
{
|
||||
// Arrange
|
||||
var namedVar1 = new Variable<int>("FirstVar", 1);
|
||||
var unnamedVar1 = new Variable<int>();
|
||||
var namedVar2 = new Variable<string>("SecondVar", "test");
|
||||
var unnamedVar2 = new Variable<bool>();
|
||||
var container = CreateContainerWithVariables(namedVar1, unnamedVar1, namedVar2, unnamedVar2);
|
||||
|
||||
// Act
|
||||
await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("FirstVar", namedVar1.Name);
|
||||
Assert.Equal("Variable1", unnamedVar1.Name);
|
||||
Assert.Equal("SecondVar", namedVar2.Name);
|
||||
Assert.Equal("Variable2", unnamedVar2.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container schedules mixed activity types")]
|
||||
public async Task Container_SchedulesMixedActivityTypes()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine("Test output");
|
||||
var testVar = new Variable<int>("testVar", 0, "testVar");
|
||||
var setVariable = new SetVariable<int>(testVar, new Input<int>(42));
|
||||
var mockActivity = Substitute.For<IActivity>();
|
||||
var container = CreateContainerWithActivities(writeLine, setVariable, mockActivity);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(writeLine));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with single activity schedules it")]
|
||||
public async Task Container_WithSingleActivity_SchedulesIt()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteLine("Single activity");
|
||||
var container = CreateContainerWithActivities(activity);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(activity));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Container with variables and activities works correctly")]
|
||||
public async Task Container_WithVariablesAndActivities_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<int>("Counter", 0);
|
||||
var activity = new WriteLine("Activity");
|
||||
var container = new TestContainer();
|
||||
container.Variables.Add(variable);
|
||||
container.Activities.Add(activity);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteContainerAsync(container);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ExpressionExecutionContext.Memory.HasBlock(variable.Id));
|
||||
Assert.True(context.HasScheduledActivity(activity));
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteContainerAsync(TestContainer container) =>
|
||||
new ActivityTestFixture(container).ExecuteAsync();
|
||||
|
||||
private static IActivity[] CreateActivities(int count) =>
|
||||
Enumerable.Range(1, count)
|
||||
.Select(i => new WriteLine($"Activity {i}"))
|
||||
.Cast<IActivity>()
|
||||
.ToArray();
|
||||
|
||||
private static List<Variable<int>> CreateUnnamedVariables(int count) =>
|
||||
Enumerable.Range(0, count)
|
||||
.Select(_ => new Variable<int>())
|
||||
.ToList();
|
||||
|
||||
private static TestContainer CreateContainerWithActivities(params IActivity[] activities)
|
||||
{
|
||||
var container = new TestContainer();
|
||||
foreach (var activity in activities)
|
||||
{
|
||||
container.Activities.Add(activity);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
private static TestContainer CreateContainerWithVariables(params Variable[] variables)
|
||||
{
|
||||
var container = new TestContainer();
|
||||
foreach (var variable in variables)
|
||||
{
|
||||
container.Variables.Add(variable);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue