From 66b94edd61aa7214d65e108b4a084d44b40fb0de Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 4 Nov 2025 10:24:16 +0100 Subject: [PATCH] FlowJoin - Improving unit tests, more DRY + integration test --- .../Branching/FlowJoinTests.cs | 228 +++++++++++++ .../Branching/FlowJoinTests.cs | 309 +++++------------- 2 files changed, 305 insertions(+), 232 deletions(-) create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs diff --git a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs new file mode 100644 index 000000000..2bc3d6b81 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowJoinTests.cs @@ -0,0 +1,228 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; +using Xunit.Abstractions; +using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers; + +namespace Elsa.Activities.IntegrationTests.Branching; + +/// +/// Integration tests for FlowJoin activity in complex flowchart scenarios. +/// +public class FlowJoinTests : IDisposable +{ + private readonly IServiceProvider _services; + private readonly CapturingTextWriter _output; + private readonly bool _originalFlowMode; + + public FlowJoinTests(ITestOutputHelper testOutputHelper) + { + _output = new(); + _services = CreateServiceProvider(testOutputHelper, _output); + _originalFlowMode = Flowchart.UseTokenFlow; + } + + public void Dispose() + { + Flowchart.UseTokenFlow = _originalFlowMode; + } + + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Handle_Complex_Flowchart_Scenarios(bool useTokenFlow, FlowJoinMode joinMode) + { + // Test with a more complex flowchart that has multiple activities + // Arrange + Flowchart.UseTokenFlow = useTokenFlow; + + var startActivity = new WriteLine("Start"); + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + var afterJoin = new WriteLine("AfterJoin"); + + var flowchart = new Flowchart + { + Start = startActivity, + Activities = { startActivity, flowJoin, afterJoin }, + Connections = + { + new() { Source = new(startActivity, "Done"), Target = new(flowJoin) }, + new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) } + } + }; + + // Act + await RunFlowchartAsync(_services, flowchart); + + // Assert + Assert.Contains("Start", _output.Lines); + + // Document expected behavior for different combinations + if (useTokenFlow) + { + // In token flow mode, both WaitAny and WaitAll should act as no-ops + // The flowchart should execute normally: Start -> FlowJoin -> AfterJoin + Assert.Contains("AfterJoin", _output.Lines); + } + else + { + // In counter flow mode: + // - WaitAny should allow continuation after first inbound connection + // - WaitAll should wait for all inbound connections (in this case just one) + // Since there's only one inbound connection, both should behave similarly + Assert.Contains("AfterJoin", _output.Lines); + } + } + + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Handle_Fork_Join_Scenarios(bool useTokenFlow, FlowJoinMode joinMode) + { + // Test FlowJoin with a Fork-Join pattern + // Arrange + Flowchart.UseTokenFlow = useTokenFlow; + + var start = new WriteLine("Start"); + var fork = new FlowFork { Branches = new(["Branch1", "Branch2"]) }; + var branch1 = new WriteLine("Branch1"); + var branch2 = new WriteLine("Branch2"); + var join = new FlowJoin { Mode = new(joinMode) }; + var afterJoin = new WriteLine("AfterJoin"); + + var flowchart = new Flowchart + { + Start = start, + Activities = { start, fork, branch1, branch2, join, afterJoin }, + Connections = + { + new() { Source = new(start, "Done"), Target = new(fork) }, + new() { Source = new(fork, "Branch1"), Target = new(branch1) }, + new() { Source = new(fork, "Branch2"), Target = new(branch2) }, + new() { Source = new(branch1, "Done"), Target = new(join) }, + new() { Source = new(branch2, "Done"), Target = new(join) }, + new() { Source = new(join, "Done"), Target = new(afterJoin) } + } + }; + + // Act + await RunFlowchartAsync(_services, flowchart); + + // Assert + Assert.Contains("Start", _output.Lines); + + if (useTokenFlow) + { + // In token flow mode, FlowJoin acts as a no-op + // Both branches should execute and continue to AfterJoin + Assert.Contains("Branch1", _output.Lines); + Assert.Contains("Branch2", _output.Lines); + Assert.Contains("AfterJoin", _output.Lines); + } + else + { + // In counter flow mode, behavior depends on join mode + if (joinMode == FlowJoinMode.WaitAny) + { + // WaitAny allows continuation after first branch completes + Assert.Contains("AfterJoin", _output.Lines); + // At least one branch should execute + Assert.True(_output.Lines.Contains("Branch1") || _output.Lines.Contains("Branch2")); + } + else // WaitAll + { + // WaitAll waits for both branches to complete + Assert.Contains("Branch1", _output.Lines); + Assert.Contains("Branch2", _output.Lines); + Assert.Contains("AfterJoin", _output.Lines); + } + } + } + + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Handle_Multiple_Join_Scenarios(bool useTokenFlow, FlowJoinMode joinMode) + { + // Test multiple FlowJoin activities in a complex flowchart + // Arrange + Flowchart.UseTokenFlow = useTokenFlow; + + var start = new WriteLine("Start"); + var fork1 = new FlowFork { Branches = new(["A", "B"]) }; + var activityA = new WriteLine("A"); + var activityB = new WriteLine("B"); + var join1 = new FlowJoin { Mode = new(joinMode) }; + var middle = new WriteLine("Middle"); + var fork2 = new FlowFork { Branches = new(["C", "D"]) }; + var activityC = new WriteLine("C"); + var activityD = new WriteLine("D"); + var join2 = new FlowJoin { Mode = new(joinMode) }; + var end = new WriteLine("End"); + + var flowchart = new Flowchart + { + Start = start, + Activities = { start, fork1, activityA, activityB, join1, middle, fork2, activityC, activityD, join2, end }, + Connections = + { + new() { Source = new(start, "Done"), Target = new(fork1) }, + new() { Source = new(fork1, "A"), Target = new(activityA) }, + new() { Source = new(fork1, "B"), Target = new(activityB) }, + new() { Source = new(activityA, "Done"), Target = new(join1) }, + new() { Source = new(activityB, "Done"), Target = new(join1) }, + new() { Source = new(join1, "Done"), Target = new(middle) }, + new() { Source = new(middle, "Done"), Target = new(fork2) }, + new() { Source = new(fork2, "C"), Target = new(activityC) }, + new() { Source = new(fork2, "D"), Target = new(activityD) }, + new() { Source = new(activityC, "Done"), Target = new(join2) }, + new() { Source = new(activityD, "Done"), Target = new(join2) }, + new() { Source = new(join2, "Done"), Target = new(end) } + } + }; + + // Act + await RunFlowchartAsync(_services, flowchart); + + // Assert + Assert.Contains("Start", _output.Lines); + Assert.Contains("Middle", _output.Lines); + Assert.Contains("End", _output.Lines); + + // Verify expected execution patterns based on flow mode and join mode + if (useTokenFlow) + { + // In token flow mode, all activities should execute + Assert.Contains("A", _output.Lines); + Assert.Contains("B", _output.Lines); + Assert.Contains("C", _output.Lines); + Assert.Contains("D", _output.Lines); + } + else + { + // In counter flow mode, execution depends on join mode + if (joinMode == FlowJoinMode.WaitAll) + { + // WaitAll ensures all parallel branches complete + Assert.Contains("A", _output.Lines); + Assert.Contains("B", _output.Lines); + Assert.Contains("C", _output.Lines); + Assert.Contains("D", _output.Lines); + } + else // WaitAny + { + // WaitAny allows continuation after first branch in each fork + // At least one from each pair should execute + Assert.True(_output.Lines.Contains("A") || _output.Lines.Contains("B")); + Assert.True(_output.Lines.Contains("C") || _output.Lines.Contains("D")); + } + } + } +} diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs index 2e8684d2c..0ab8e6d86 100644 --- a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs @@ -10,86 +10,6 @@ namespace Elsa.Activities.UnitTests.Branching; /// public class FlowJoinTests { - [Theory] - [InlineData(FlowJoinMode.WaitAny)] - [InlineData(FlowJoinMode.WaitAll)] - public async Task Should_Complete_Activity_In_Token_Flow_Mode(FlowJoinMode joinMode) - { - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = true; - - try - { - var flowJoin = new FlowJoin { Mode = new(joinMode) }; - - // Act - var context = await ExecuteAsync(flowJoin); - - // Assert - Assert.True(context.IsCompleted); - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - } - - [Theory] - [InlineData(FlowJoinMode.WaitAny)] - [InlineData(FlowJoinMode.WaitAll)] - public async Task Should_Complete_Activity_In_Counter_Flow_Mode(FlowJoinMode joinMode) - { - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = false; - - try - { - var flowJoin = new FlowJoin { Mode = new(joinMode) }; - - // Act - Execute without parent context (simple case) - var context = await ExecuteAsync(flowJoin); - - // Assert - Assert.True(context.IsCompleted); - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - } - - [Fact] - public async Task Should_Execute_In_Flowchart_Context() - { - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = false; - - try - { - var flowJoin = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) }; - var flowchart = new Flowchart - { - Start = flowJoin, - Activities = { flowJoin } - }; - - // Act - var context = await ExecuteFlowchartAsync(flowchart); - - // Assert - Assert.NotNull(context); - Assert.True(context.HasScheduledActivity(flowJoin)); - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - } - - // Comprehensive tests covering all flow mode combinations [Theory] [InlineData(true, FlowJoinMode.WaitAny)] [InlineData(true, FlowJoinMode.WaitAll)] @@ -97,38 +17,11 @@ public class FlowJoinTests [InlineData(false, FlowJoinMode.WaitAll)] public async Task Should_Complete_In_All_Flow_Mode_Combinations(bool useTokenFlow, FlowJoinMode joinMode) { - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = useTokenFlow; + // Arrange & Act + var context = await ExecuteWithFlowModeAsync(useTokenFlow, joinMode); - try - { - var flowJoin = new FlowJoin { Mode = new(joinMode) }; - - // Act - var context = await ExecuteAsync(flowJoin); - - // Assert - Assert.True(context.IsCompleted); - - // Document expected behavior based on flow mode - if (useTokenFlow) - { - // In token flow mode, FlowJoin acts as a no-op and always completes - // regardless of join mode (WaitAny or WaitAll) - Assert.True(context.IsCompleted, "Token flow mode should always complete"); - } - else - { - // In counter flow mode, behavior depends on join mode and flowchart state - // Without parent context, both WaitAny and WaitAll should complete - Assert.True(context.IsCompleted, "Counter flow mode should complete when no parent context"); - } - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } + // Assert + Assert.True(context.IsCompleted); } [Theory] @@ -138,139 +31,85 @@ public class FlowJoinTests [InlineData(false, FlowJoinMode.WaitAll)] public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(bool useTokenFlow, FlowJoinMode joinMode) { - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = useTokenFlow; + // Arrange & Act + var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(useTokenFlow, joinMode); - try - { - var flowJoin = new FlowJoin { Mode = new(joinMode) }; - var flowchart = new Flowchart - { - Start = flowJoin, - Activities = { flowJoin } - }; + // Assert + Assert.NotNull(context); + + var expectedMessage = useTokenFlow + ? $"Token flow mode with {joinMode} should schedule the join activity" + : $"Counter flow mode with {joinMode} should schedule the join activity as start"; - // Act - var context = await ExecuteFlowchartAsync(flowchart); - - // Assert - Assert.NotNull(context); - - if (useTokenFlow) - { - // In token flow mode, the join should be scheduled regardless of join mode - Assert.True(context.HasScheduledActivity(flowJoin), - $"Token flow mode with {joinMode} should schedule the join activity"); - } - else - { - // In counter flow mode, scheduling depends on join mode and flowchart state - // Since this is a simple flowchart with just the join as start activity, - // it should be scheduled - Assert.True(context.HasScheduledActivity(flowJoin), - $"Counter flow mode with {joinMode} should schedule the join activity as start"); - } - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - } - - [Theory] - [InlineData(true, FlowJoinMode.WaitAny)] - [InlineData(true, FlowJoinMode.WaitAll)] - [InlineData(false, FlowJoinMode.WaitAny)] - [InlineData(false, FlowJoinMode.WaitAll)] - public async Task Should_Handle_Complex_Flowchart_Scenarios(bool useTokenFlow, FlowJoinMode joinMode) - { - // Test with a more complex flowchart that has multiple activities - // Arrange - var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = useTokenFlow; - - try - { - var startActivity = new WriteLine("Start"); - var flowJoin = new FlowJoin { Mode = new(joinMode) }; - var afterJoin = new WriteLine("AfterJoin"); - - var flowchart = new Flowchart - { - Start = startActivity, - Activities = { startActivity, flowJoin, afterJoin }, - Connections = - { - new() { Source = new(startActivity, "Done"), Target = new(flowJoin) }, - new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) } - } - }; - - // Act - var context = await ExecuteFlowchartAsync(flowchart); - - // Assert - Assert.NotNull(context); - - // The start activity should always be scheduled first - Assert.True(context.HasScheduledActivity(startActivity), - "Start activity should be scheduled"); - - // Document expected behavior for different combinations - if (useTokenFlow) - { - // In token flow mode, both WaitAny and WaitAll act as no-ops - // The exact scheduling depends on the flowchart execution engine - } - else - { - // In counter flow mode: - // - WaitAny should allow continuation after first inbound connection - // - WaitAll should wait for all inbound connections (in this case just one) - // Since there's only one inbound connection, both should behave similarly - } - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } + Assert.True(context.HasScheduledActivity(flowJoin), expectedMessage); } [Fact] public async Task Should_Demonstrate_Token_vs_Counter_Flow_Differences() { - // This test demonstrates the key differences between token and counter flow modes + // Arrange var joinMode = FlowJoinMode.WaitAll; // Use WaitAll to highlight differences - // Test Token Flow Mode + // Act + var tokenContext = await ExecuteWithFlowModeAsync(true, joinMode); + var counterContext = await ExecuteWithFlowModeAsync(false, joinMode); + + // Assert + Assert.True(tokenContext.IsCompleted, "Token flow should always complete"); + Assert.True(counterContext.IsCompleted, "Counter flow should complete without parent context"); + } + + /// + /// Creates a FlowJoin activity with the specified mode. + /// + private static FlowJoin CreateFlowJoin(FlowJoinMode joinMode) => new() { Mode = new(joinMode) }; + + /// + /// Creates a simple flowchart with the FlowJoin as the start activity. + /// + private static Flowchart CreateSimpleFlowchart(FlowJoin flowJoin) => new() + { + Start = flowJoin, + Activities = { flowJoin } + }; + + /// + /// Executes a FlowJoin activity with the specified flow mode, handling the UseTokenFlow setup and teardown. + /// + private static async Task ExecuteWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode) + { + return await WithFlowModeAsync(useTokenFlow, async () => + { + var flowJoin = CreateFlowJoin(joinMode); + return await ExecuteAsync(flowJoin); + }); + } + + /// + /// Executes a FlowJoin activity within a flowchart context with the specified flow mode. + /// + private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode) + { + return await WithFlowModeAsync(useTokenFlow, async () => + { + var flowJoin = CreateFlowJoin(joinMode); + var flowchart = CreateSimpleFlowchart(flowJoin); + var context = await ExecuteFlowchartAsync(flowchart); + return (context, flowJoin); + }); + } + + /// + /// Executes an action with the specified flow mode, ensuring proper setup and teardown of UseTokenFlow. + /// + private static async Task WithFlowModeAsync(bool useTokenFlow, Func> action) + { var originalValue = Flowchart.UseTokenFlow; - Flowchart.UseTokenFlow = true; - + Flowchart.UseTokenFlow = useTokenFlow; + try { - var tokenFlowJoin = new FlowJoin { Mode = new(joinMode) }; - var tokenContext = await ExecuteAsync(tokenFlowJoin); - - // Token flow: FlowJoin acts as no-op, always completes - Assert.True(tokenContext.IsCompleted, "Token flow should always complete"); - } - finally - { - Flowchart.UseTokenFlow = originalValue; - } - - // Test Counter Flow Mode - Flowchart.UseTokenFlow = false; - - try - { - var counterFlowJoin = new FlowJoin { Mode = new(joinMode) }; - var counterContext = await ExecuteAsync(counterFlowJoin); - - // Counter flow: Behavior depends on CanWaitAllProceed for WaitAll mode - // Without parent context, it should complete - Assert.True(counterContext.IsCompleted, "Counter flow should complete without parent context"); + return await action(); } finally { @@ -278,11 +117,17 @@ public class FlowJoinTests } } + /// + /// Executes an activity using the ActivityTestFixture. + /// private static async Task ExecuteAsync(IActivity activity) { return await new ActivityTestFixture(activity).ExecuteAsync(); } + /// + /// Executes a flowchart using the ActivityTestFixture. + /// private static async Task ExecuteFlowchartAsync(Flowchart flowchart) { return await new ActivityTestFixture(flowchart).ExecuteAsync();