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:
parent
d608b9de14
commit
e261a91127
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue