Add unit and integration tests for FlowDecision activity to validate branching and nested workflow logic (#7117)

* Add unit and integration tests for `FlowDecision` activity to validate branching and nested workflow logic

- Included integration tests covering `FlowDecision` behavior for conditions, nested decisions, single-path scenarios, and path convergence in flowcharts.
- Added unit tests to verify correctness of outcomes, default behavior, and condition evaluation logic.

* Refactor `FlowDecisionTests` to improve parameterized test coverage, streamline assertions, and enhance test organization by introducing reusable data providers.

* Add `[Collection("FlowchartTests")]` annotation to `FlowDecisionTests` to improve test grouping and execution consistency.
This commit is contained in:
Sipke Schoorstra 2025-11-27 20:57:38 +01:00 committed by GitHub
parent d608b9de14
commit e261a91127
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 280 additions and 0 deletions

View file

@ -0,0 +1,206 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests.Branching;
/// <summary>
/// Integration tests for FlowDecision activity in flowchart scenarios.
/// </summary>
[Collection("FlowchartTests")]
public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable
{
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
private readonly bool _originalFlowMode = Flowchart.UseTokenFlow;
public void Dispose()
{
Flowchart.UseTokenFlow = _originalFlowMode;
}
[Theory(DisplayName = "FlowDecision follows correct path based on condition")]
[MemberData(nameof(BasicPathTestCases))]
public async Task Should_Follow_Correct_Path_Based_On_Condition(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
{
// Arrange
Flowchart.UseTokenFlow = useTokenFlow;
var start = new WriteLine("Start");
var decision = new FlowDecision(ctx => condition);
var truePath = new WriteLine("TruePath");
var falsePath = new WriteLine("FalsePath");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, decision, truePath, falsePath },
Connections =
{
new() { Source = new(start, "Done"), Target = new(decision) },
new() { Source = new(decision, "True"), Target = new(truePath) },
new() { Source = new(decision, "False"), Target = new(falsePath) }
}
};
// Act
await _fixture.RunActivityAsync(flowchart);
// Assert
AssertOutputs(expectedOutputs, unexpectedOutputs);
}
public static IEnumerable<object[]> BasicPathTestCases()
{
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
yield return [true, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }];
yield return [true, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }];
yield return [false, true, new[] { "Start", "TruePath" }, new[] { "FalsePath" }];
yield return [false, false, new[] { "Start", "FalsePath" }, new[] { "TruePath" }];
}
[Theory(DisplayName = "FlowDecision handles nested decisions")]
[MemberData(nameof(NestedDecisionTestCases))]
public async Task Should_Handle_Nested_Decisions(bool useTokenFlow, bool outerCondition, bool innerCondition, string[] expectedOutputs, string[] unexpectedOutputs)
{
// Arrange
Flowchart.UseTokenFlow = useTokenFlow;
var start = new WriteLine("Start");
var outerDecision = new FlowDecision(ctx => outerCondition);
var innerDecision = new FlowDecision(ctx => innerCondition);
var innerTrue = new WriteLine("InnerTrue");
var innerFalse = new WriteLine("InnerFalse");
var outerFalse = new WriteLine("OuterFalse");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, outerDecision, innerDecision, innerTrue, innerFalse, outerFalse },
Connections =
{
new() { Source = new(start, "Done"), Target = new(outerDecision) },
new() { Source = new(outerDecision, "True"), Target = new(innerDecision) },
new() { Source = new(outerDecision, "False"), Target = new(outerFalse) },
new() { Source = new(innerDecision, "True"), Target = new(innerTrue) },
new() { Source = new(innerDecision, "False"), Target = new(innerFalse) }
}
};
// Act
await _fixture.RunActivityAsync(flowchart);
// Assert
AssertOutputs(expectedOutputs, unexpectedOutputs);
}
public static IEnumerable<object[]> NestedDecisionTestCases()
{
// useTokenFlow, outerCondition, innerCondition, expectedOutputs, unexpectedOutputs
yield return [true, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }];
yield return [true, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }];
yield return [true, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
yield return [true, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
yield return [false, true, true, new[] { "Start", "InnerTrue" }, new[] { "InnerFalse", "OuterFalse" }];
yield return [false, true, false, new[] { "Start", "InnerFalse" }, new[] { "InnerTrue", "OuterFalse" }];
yield return [false, false, true, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
yield return [false, false, false, new[] { "Start", "OuterFalse" }, new[] { "InnerTrue", "InnerFalse" }];
}
[Theory(DisplayName = "FlowDecision works with only one path connected")]
[MemberData(nameof(OnePathConnectedTestCases))]
public async Task Should_Work_With_Only_One_Path_Connected(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
{
// Arrange
Flowchart.UseTokenFlow = useTokenFlow;
var start = new WriteLine("Start");
var decision = new FlowDecision(ctx => condition);
var truePath = new WriteLine("TruePath");
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, decision, truePath, end },
Connections =
{
new() { Source = new(start, "Done"), Target = new(decision) },
new() { Source = new(decision, "True"), Target = new(truePath) },
// False path not connected
new() { Source = new(truePath, "Done"), Target = new(end) }
}
};
// Act
await _fixture.RunActivityAsync(flowchart);
// Assert
AssertOutputs(expectedOutputs, unexpectedOutputs);
}
public static IEnumerable<object[]> OnePathConnectedTestCases()
{
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
yield return [true, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
yield return [true, false, new[] { "Start" }, new[] { "TruePath", "End" }];
yield return [false, true, new[] { "Start", "TruePath", "End" }, Array.Empty<string>()];
yield return [false, false, new[] { "Start" }, new[] { "TruePath", "End" }];
}
[Theory(DisplayName = "FlowDecision converges paths correctly")]
[MemberData(nameof(ConvergePathsTestCases))]
public async Task Should_Converge_Paths_Correctly(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
{
// Arrange
Flowchart.UseTokenFlow = useTokenFlow;
var start = new WriteLine("Start");
var decision = new FlowDecision(ctx => condition);
var truePath = new WriteLine("TruePath");
var falsePath = new WriteLine("FalsePath");
var converge = new WriteLine("Converge");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, decision, truePath, falsePath, converge },
Connections =
{
new() { Source = new(start, "Done"), Target = new(decision) },
new() { Source = new(decision, "True"), Target = new(truePath) },
new() { Source = new(decision, "False"), Target = new(falsePath) },
new() { Source = new(truePath, "Done"), Target = new(converge) },
new() { Source = new(falsePath, "Done"), Target = new(converge) }
}
};
// Act
await _fixture.RunActivityAsync(flowchart);
// Assert
AssertOutputs(expectedOutputs, unexpectedOutputs);
}
public static IEnumerable<object[]> ConvergePathsTestCases()
{
// useTokenFlow, condition, expectedOutputs, unexpectedOutputs
yield return [true, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }];
yield return [true, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }];
yield return [false, true, new[] { "Start", "TruePath", "Converge" }, new[] { "FalsePath" }];
yield return [false, false, new[] { "Start", "FalsePath", "Converge" }, new[] { "TruePath" }];
}
private void AssertOutputs(string[] expectedOutputs, string[] unexpectedOutputs)
{
foreach (var expected in expectedOutputs)
{
Assert.Contains(expected, _fixture.CapturingTextWriter.Lines);
}
foreach (var unexpected in unexpectedOutputs)
{
Assert.DoesNotContain(unexpected, _fixture.CapturingTextWriter.Lines);
}
}
}

View file

@ -0,0 +1,74 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Activities;
namespace Elsa.Activities.UnitTests.Branching;
public class FlowDecisionTests
{
[Theory(DisplayName = "FlowDecision produces correct outcome and completes successfully")]
[InlineData(true, "True")]
[InlineData(false, "False")]
public async Task Should_Produce_Correct_Outcome_And_Complete(bool condition, string expectedOutcome)
{
// Arrange
var flowDecision = new FlowDecision(ctx => condition);
// Act
var context = await ExecuteAsync(flowDecision);
// Assert
Assert.Equal(ActivityStatus.Completed, context.Status);
Assert.True(context.HasOutcome(expectedOutcome));
}
[Fact(DisplayName = "FlowDecision defaults to False when no condition is set")]
public async Task Should_Default_To_False_When_No_Condition_Is_Set()
{
// Arrange
var flowDecision = new FlowDecision();
// Act
var context = await ExecuteAsync(flowDecision);
// Assert
Assert.True(context.HasOutcome("False"));
}
[Fact(DisplayName = "FlowDecision evaluates condition exactly once")]
public async Task Should_Evaluate_Condition_Exactly_Once()
{
// Arrange
var count = 0;
var flowDecision = new FlowDecision(ctx => { count++; return true; });
// Act
await ExecuteAsync(flowDecision);
// Assert
Assert.Equal(1, count);
}
[Fact(DisplayName = "FlowDecision uses latest captured state when evaluating condition")]
public async Task Should_Use_Latest_Captured_State_When_Evaluating_Condition()
{
// Arrange
var flag = false;
// ReSharper disable once AccessToModifiedClosure
var flowDecision = new FlowDecision(ctx => flag);
// Mutate after construction, before execution
flag = true;
// Act
var context = await ExecuteAsync(flowDecision);
// Assert
Assert.True(context.HasOutcome("True"));
}
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
{
return new ActivityTestFixture(activity).ExecuteAsync();
}
}