From d0a4520e7d04977bc8bf55fdad54bf68a42b5430 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Oct 2025 15:36:12 +0200 Subject: [PATCH] Adds unit tests for FlowSwitch activity (#6996) * Add unit tests for `FlowFork` activity and enhance test utilities - Introduce `FlowForkTests` to validate activity behavior with various branch configurations. - Extend `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes. - Modify `ActivityTestFixture` to ensure activities transition to `Running` status before execution. * Add unit tests for `FlowSwitch` activity and extend test utilities - Implement `FlowSwitchTests` to verify behavior based on switch cases, modes, and literal expressions. - Enhance `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes in execution context. * Update test guidelines to include examples for checking activity outcomes - Add unit test examples for validating multiple and default outcomes. - Extend documentation to describe new `ActivityExecutionContextExtensions` methods: `GetOutcomes` and `HasOutcome`. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `GetOutcomes` method in `ActivityExecutionContextExtensions`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- doc/qa/test-guidelines.md | 38 +++++++ .../ActivityExecutionContextExtensions.cs | 19 +++- .../Branching/FlowSwitchTests.cs | 99 +++++++++++++++++++ 3 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index de0b29cc1..81feb0519 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -160,6 +160,42 @@ public async Task Should_Schedule_Child_Activity() } ``` +**Example (checking activity outcomes):** +```csharp +[Fact] +public async Task Should_Return_Multiple_Outcomes() +{ + // Arrange + var flowFork = new FlowFork + { + Branches = new(new[] { "Branch1", "Branch2", "Branch3" }) + }; + + // Act + var context = await new ActivityTestFixture(flowFork).ExecuteAsync(); + + // Assert - Check all outcomes + var outcomes = context.GetOutcomes().ToList(); + Assert.Equal(3, outcomes.Count); + Assert.Contains("Branch1", outcomes); + Assert.Contains("Branch2", outcomes); + Assert.Contains("Branch3", outcomes); +} + +[Fact] +public async Task Should_Return_Default_Outcome() +{ + // Arrange + var flowSwitch = new FlowSwitch(); + + // Act + var context = await new ActivityTestFixture(flowSwitch).ExecuteAsync(); + + // Assert - Check single outcome + Assert.True(context.HasOutcome("Default")); +} +``` + #### **Integration tests:** - Place the activity inside a minimal workflow definition and run via [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs). Assert outputs/variables and that the activity integrates correctly with preceding/following activities. - If activity creates bookmarks or relies on scheduler semantics, integration tests should resume bookmarks via the engine APIs to validate resumption. @@ -214,6 +250,8 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status); | `ActivityTestFixture.ExecuteAsync` | Execute activity and return context | Standard activity unit test execution | | `context.GetActivityOutput` | Get output from activity using expression selector | Asserting activity outputs in unit tests | | `context.HasScheduledActivity` | Check if activity is scheduled | Verifying scheduling behavior in unit tests | +| `context.GetOutcomes` | Get all outcomes from activity execution | Asserting multiple outcomes in unit tests | +| `context.HasOutcome` | Check if activity has specific outcome | Asserting single outcome in unit tests | | `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests | | `RunActivityExtensions.RunActivityAsync` | Run single activity as workflow | Integration tests for single activities | | `PopulateRegistriesAsync` | Register types for JSON deserialization | Loading JSON workflows.
Integration tests only | diff --git a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs index 6af31b080..3f9d03d9d 100644 --- a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs +++ b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs @@ -17,14 +17,23 @@ public static class ActivityExecutionContextExtensions { return activityExecutionContext.WorkflowExecutionContext.Scheduler.Find(x => x.Activity == activity) != null; } - + + /// + /// Retrieves the collection of outcomes recorded in the execution context's journal data. + /// + /// The activity execution context from which to retrieve the outcomes. + /// A collection of outcome strings, or an empty collection if no outcomes are present. public static IEnumerable GetOutcomes(this ActivityExecutionContext activityExecutionContext) { - if (activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomesObj) && outcomesObj is string[] outcomes) - return outcomes; - return []; + return activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomes) && outcomes is string[] arr ? arr : []; } - + + /// + /// Determines whether the specified outcome exists in the current activity execution context. + /// + /// The activity execution context to check for the outcome. + /// The outcome to verify within the execution context. + /// True if the specified outcome exists; otherwise, false. public static bool HasOutcome(this ActivityExecutionContext activityExecutionContext, string outcome) { return activityExecutionContext.GetOutcomes().Contains(outcome); diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs new file mode 100644 index 000000000..bbaff3172 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs @@ -0,0 +1,99 @@ +using Elsa.Expressions.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; + +namespace Elsa.Activities.UnitTests.Branching; + +public class FlowSwitchTests +{ + [Theory] + [MemberData(nameof(DefaultOutcomeTestCases))] + public async Task Should_Return_Default_When_No_Cases_Match(List cases) + { + // Arrange + var flowSwitch = new FlowSwitch { Cases = cases }; + + // Act + var context = await ExecuteAsync(flowSwitch); + + // Assert - Activity should return Default outcome when no cases match. + Assert.True(context.HasOutcome("Default")); + } + + [Theory] + [MemberData(nameof(SwitchModeTestCases))] + public async Task Should_Return_Outcomes_Based_On_Switch_Mode( + List cases, + SwitchMode mode, + string[] expectedOutcomes) + { + // Arrange + var flowSwitch = new FlowSwitch + { + Cases = cases, + Mode = new(mode) + }; + + // Act + var context = await ExecuteAsync(flowSwitch); + + // Assert - Activity should return outcomes based on switch mode. + var outcomes = context.GetOutcomes().ToList(); + Assert.Equal(expectedOutcomes.Length, outcomes.Count); + foreach (var expectedOutcome in expectedOutcomes) + Assert.Contains(expectedOutcome, outcomes); + } + + [Fact] + public async Task Should_Evaluate_Cases_With_Literal_Expression() + { + // Arrange + var flowSwitch = new FlowSwitch + { + Cases = new List + { + new("TrueCase", Expression.LiteralExpression(true)), + new("FalseCase", Expression.LiteralExpression(false)) + } + }; + + // Act + var context = await ExecuteAsync(flowSwitch); + + // Assert - Activity should match the case with true literal. + Assert.True(context.HasOutcome("TrueCase")); + } + + public static IEnumerable DefaultOutcomeTestCases() + { + yield return [new List()]; + yield return + [ + new List + { + new("Case1", () => false), + new("Case2", () => false) + } + ]; + } + + public static IEnumerable SwitchModeTestCases() + { + var cases = new List + { + new("Case1", () => false), + new("Case2", () => true), + new("Case3", () => true) + }; + + yield return [cases, SwitchMode.MatchFirst, new[] { "Case2" }]; + yield return [cases, SwitchMode.MatchAny, new[] { "Case2", "Case3" }]; + } + + private static Task ExecuteAsync(IActivity activity) + { + return new ActivityTestFixture(activity).ExecuteAsync(); + } +}