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:
Sipke Schoorstra 2025-11-25 19:06:52 +01:00 committed by GitHub
parent faebea76a0
commit e936d636ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 520 additions and 0 deletions

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

View file

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

View file

@ -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;
}
}

View file

@ -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;
}
}