Adds SetVariable activity unit tests (#6989)

* Update doc/qa/test-guidelines.md

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* Improvements on maintainability of sendhttprequest unit tests

* Improving tests and scheduled activity evaluation for activity context

* Refactor and splitting unnecessary grouped tests

* Improvements on SendHttp Unit tests

* Improvements on tests

* Update test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Small suggestions from copilot

* Update test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* small copilot suggestion

* Introduce Scheduler Strategy Interfaces and Implementations for Workflow and Activity Execution Contexts

- Added `IWorkflowExecutionContextSchedulerStrategy` and `IActivityExecutionContextSchedulerStrategy` interfaces.
- Implemented `WorkflowExecutionContextSchedulerStrategy` and `ActivityExecutionContextSchedulerStrategy` for scheduling activities in workflows.
- Refactored scheduling logic to utilize the new scheduler strategies.
- Updated unit tests and test helpers to reflect refactoring, introducing fake implementations for testing purposes.
- Adjusted background execution scheduling and improved extensibility for custom scheduler strategies.

* Refactor `SendHttpRequestTests`: simplify scheduling assertions, use shared extensions, and standardize method naming. Streamline helper methods and remove unused test logic.

* Refactor: Replace `ActivityTestHelper` with `ActivityTestFixture` in unit tests for streamlined activity testing

- Introduced `ActivityTestFixture` with a fluent API for better test setup and execution of activities.
- Added extension methods `ActivityTestFixtureExtensions` and `ActivityTestFixtureHttpExtensions` for configuring attributes and HTTP services.
- Updated test guidelines and unit tests to use the new fixture and extensions.
- Removed `ActivityTestHelper`.

* Refactor: Move `ActivityTestFixture` and related extensions to shared project for reuse across test suites

- Consolidated `ActivityTestFixture`, `ActivityTestFixtureExtensions`, and `ActivityTestFixtureHttpExtensions` into `Elsa.Testing.Shared`.
- Updated namespaces and imports across unit tests to reflect new structure.
- Enhanced `AssertActivityAttributes` and added fluent configuration APIs.
- Adjusted `Directory.Packages.props` with new dependencies, including `NSubstitute` and `xunit.assert`.

* Refactor `SetVariableTests`: inline `ActivityTestFixture` initialization to simplify test setup.

* Refactor `ActivityTestFixture`: eliminate redundant field `_services`, add `UsedImplicitly` attributes, and improve service collection management

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for `ActivityExecutionContextExtensions`, detailing methods and parameters.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for scheduler strategies and their methods

- Updated `IActivityExecutionContextSchedulerStrategy` and `IWorkflowExecutionContextSchedulerStrategy` interfaces with XML summaries for methods.
- Added XML documentation to implementations (`ActivityExecutionContextSchedulerStrategy`, `WorkflowExecutionContextSchedulerStrategy`) and test fakes for clarity.

* Refactor `WriteLineTests`: consolidate duplicate test logic, simplify setup with shared helper method, and enhance readability in assertions.

* Refactor `WriteLineTests` and `SetVariableTests`: replace `WriteLineAsync` assertions with synchronous `WriteLine`, streamline exception recording in `SetVariableTests`, and remove unused imports.

* Add integration and unit tests for `SetVariable` activity: ensure variable scoping, null handling, and value setting are covered.

* Add new `SetVariableOfTTests` and update `SetVariableTests` for improved test coverage and type handling.

* Add new unit and integration tests for activity input and expression evaluation

- Introduced comprehensive test suites covering activity input evaluation, expression handling, and fault scenarios.
- Added unit tests for `ExpressionDescriptorRegistry`, `ExpressionEvaluator`, and activity execution context extensions.
- Added integration tests: `CustomInputEvaluatorTests`, `InputEvaluationErrorTests`, and `InputPropertyEvaluationTests`.
- Enhanced code coverage for edge cases and async evaluation logic.

* Remove unused `RunWorkflowAsync` extension method from `RunActivityExtensions`.

* Rename `CustomInputEvaluatorTests` to `InputEvaluationTests` for consistency with naming conventions.

* Remove unused `Elsa.Workflows.Activities` import from `RunActivityExtensions`.

* Remove redundant comments from `InputEvaluationErrorTests` for clarity.

* Refactor unit tests to streamline activity and expression evaluations

- Refactored `ExecuteActivityAsync` and `ExecuteWriteLineAsync` into shared helpers for consistency and reuse across tests.
- Replaced redundant mock setups with helper methods in `ExpressionDescriptorRegistryTests`.
- Simplified test setup for expression and activity evaluation by removing unused imports and consolidating configuration logic.
- Enhanced readability by reducing duplicate code and leveraging shared utility methods.

* Refactor activity input evaluation tests

- Extracted `CreateContextAsync` helper into `EvaluationTestHelpers` for reuse across evaluation test suites.
- Replaced inline activity context setup with shared helper in `InputPropertyEvaluationTests`, `WrappedInputEvaluationTests`, and related test suites.
- Simplified test method names for clarity and consistency.
- Updated test annotations to enhance readability and align with naming conventions.

* Remove redundant test cases and unused imports

- Deleted duplicated and non-essential test cases across evaluation test suites.
- Removed unused imports to improve code cleanliness and readability.
- Streamlined variable initializations and method calls within test setups.

* Remove redundant test case from `InputEvaluationErrorTests`

- Deleted the `ContinuesEvaluationForMultipleInputs` test, as it overlaps with existing tests and does not provide additional coverage.

* Remove redundant assertion from `InputPropertyEvaluationTests`

- Deleted `Assert.True(context.GetHasEvaluatedProperties())`, as it is unnecessary for verifying test outcomes.

* Remove redundant test cases from `WrappedInputEvaluationTests`

- Deleted `UsesDefaultValueWhenInputIsNull` and `EvaluatesExpression` tests as they are either duplicated or unnecessary for current test coverage.

* Add unit test projects for `Elsa.Workflows.Management` and `Elsa.Expressions`

- Introduced new test projects to separate and organize unit tests for `Elsa.Workflows.Management` and `Elsa.Expressions`.
- Updated `Elsa.sln` to include references to the newly added test projects.
- Adjusted namespaces in affected test classes for consistency with the updated project structure.

* Refactor `ExpressionEvaluatorTests` for clarity and consistency

- Simplified test method names and annotations for improved readability.
- Replaced duplicate mock setups with helper functions (`CreateContextAsync`, `CreateContextWithMockHandlerAsync`, and related methods).
- Streamlined test setups by removing redundant code and consolidating context creation logic.
- Updated test annotations to include descriptive `DisplayName` attributes.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove redundant comment from `ExpressionEvaluatorTests` for clarity

* Remove redundant blank lines from unit test classes

- Eliminated unnecessary blank lines across `ActivityExecutionContextExtensions` test suites to improve code readability and consistency.

* Add unit tests for Flowchart activity

- Introduced `FlowchartTests` to verify common Flowchart behavior, including start activity scheduling, execution without a start activity, and `UseTokenFlow` handling.
- Added `FlowchartTestHelpers` to encapsulate shared test logic.
- Updated project file to include the new `Flow` folder for organization.

* Add integration tests for Flowchart activity execution strategies

- Introduced `FlowchartCounterBasedTests` and `FlowchartTokenBasedTests` to verify different Flowchart execution strategies.
- Added `FlowchartTestHelpers` for shared test logic, including helper methods for creating various flowchart configurations and connections.
- Enhanced test coverage with scenarios for linear, parallel, and conditional flows, mixed merge modes, nested flowcharts, and token handling.
- Updated project structure to include new test classes under the `Flow` folder.

* Remove obsolete tag from `FlowJoin` activity description

* Handle both string and enum values in `GetMergeMode` for backwards compatibility

* Group flowchart integration tests into non-parallelizable test collection

- Introduced `FlowchartTestCollection` to prevent parallel execution of flowchart tests due to shared `Flowchart.UseTokenFlow` flag.
- Updated `FlowchartCounterBasedTests` and `FlowchartTokenBasedTests` to implement `IDisposable` and manage `UseTokenFlow` cleanup.

* Remove unused `Flow` folder reference from test project file

* Add `ParallelTests` for unit and integration testing with various scenarios (#6988)

- Added unit tests for `Parallel` activity to ensure proper scheduling of child activities, including empty and mixed activity cases.
- Added integration tests to validate execution flow and edge cases for `Parallel` activities (e.g., nested parallelism, fault handling).
- Enhanced `ScheduleChildrenAsync` in `Parallel` to handle no activity scenario by completing immediately.

* Fix null-check and memory declaration in `ActivityTestFixture` to prevent potential `NullReferenceException`.

* Introduce Scheduler Strategy Interfaces and Implementations for Workflow and Activity Execution Contexts (#6984)

* Introduce Scheduler Strategy Interfaces and Implementations for Workflow and Activity Execution Contexts

- Added `IWorkflowExecutionContextSchedulerStrategy` and `IActivityExecutionContextSchedulerStrategy` interfaces.
- Implemented `WorkflowExecutionContextSchedulerStrategy` and `ActivityExecutionContextSchedulerStrategy` for scheduling activities in workflows.
- Refactored scheduling logic to utilize the new scheduler strategies.
- Updated unit tests and test helpers to reflect refactoring, introducing fake implementations for testing purposes.
- Adjusted background execution scheduling and improved extensibility for custom scheduler strategies.

* Refactor `SendHttpRequestTests`: simplify scheduling assertions, use shared extensions, and standardize method naming. Streamline helper methods and remove unused test logic.

* Refactor: Replace `ActivityTestHelper` with `ActivityTestFixture` in unit tests for streamlined activity testing

- Introduced `ActivityTestFixture` with a fluent API for better test setup and execution of activities.
- Added extension methods `ActivityTestFixtureExtensions` and `ActivityTestFixtureHttpExtensions` for configuring attributes and HTTP services.
- Updated test guidelines and unit tests to use the new fixture and extensions.
- Removed `ActivityTestHelper`.

* Refactor: Move `ActivityTestFixture` and related extensions to shared project for reuse across test suites

- Consolidated `ActivityTestFixture`, `ActivityTestFixtureExtensions`, and `ActivityTestFixtureHttpExtensions` into `Elsa.Testing.Shared`.
- Updated namespaces and imports across unit tests to reflect new structure.
- Enhanced `AssertActivityAttributes` and added fluent configuration APIs.
- Adjusted `Directory.Packages.props` with new dependencies, including `NSubstitute` and `xunit.assert`.

* Refactor `SetVariableTests`: inline `ActivityTestFixture` initialization to simplify test setup.

* Refactor `ActivityTestFixture`: eliminate redundant field `_services`, add `UsedImplicitly` attributes, and improve service collection management

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for `ActivityExecutionContextExtensions`, detailing methods and parameters.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for scheduler strategies and their methods

- Updated `IActivityExecutionContextSchedulerStrategy` and `IWorkflowExecutionContextSchedulerStrategy` interfaces with XML summaries for methods.
- Added XML documentation to implementations (`ActivityExecutionContextSchedulerStrategy`, `WorkflowExecutionContextSchedulerStrategy`) and test fakes for clarity.

* Refactor `WriteLineTests`: consolidate duplicate test logic, simplify setup with shared helper method, and enhance readability in assertions.

* Refactor `WriteLineTests` and `SetVariableTests`: replace `WriteLineAsync` assertions with synchronous `WriteLine`, streamline exception recording in `SetVariableTests`, and remove unused imports.

* Add `ParallelTests` for unit and integration testing with various scenarios (#6988)

- Added unit tests for `Parallel` activity to ensure proper scheduling of child activities, including empty and mixed activity cases.
- Added integration tests to validate execution flow and edge cases for `Parallel` activities (e.g., nested parallelism, fault handling).
- Enhanced `ScheduleChildrenAsync` in `Parallel` to handle no activity scenario by completing immediately.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add new unit and integration tests for activity input and expression evaluation (#6990)

* Introduce Scheduler Strategy Interfaces and Implementations for Workflow and Activity Execution Contexts

- Added `IWorkflowExecutionContextSchedulerStrategy` and `IActivityExecutionContextSchedulerStrategy` interfaces.
- Implemented `WorkflowExecutionContextSchedulerStrategy` and `ActivityExecutionContextSchedulerStrategy` for scheduling activities in workflows.
- Refactored scheduling logic to utilize the new scheduler strategies.
- Updated unit tests and test helpers to reflect refactoring, introducing fake implementations for testing purposes.
- Adjusted background execution scheduling and improved extensibility for custom scheduler strategies.

* Refactor `SendHttpRequestTests`: simplify scheduling assertions, use shared extensions, and standardize method naming. Streamline helper methods and remove unused test logic.

* Refactor: Replace `ActivityTestHelper` with `ActivityTestFixture` in unit tests for streamlined activity testing

- Introduced `ActivityTestFixture` with a fluent API for better test setup and execution of activities.
- Added extension methods `ActivityTestFixtureExtensions` and `ActivityTestFixtureHttpExtensions` for configuring attributes and HTTP services.
- Updated test guidelines and unit tests to use the new fixture and extensions.
- Removed `ActivityTestHelper`.

* Refactor: Move `ActivityTestFixture` and related extensions to shared project for reuse across test suites

- Consolidated `ActivityTestFixture`, `ActivityTestFixtureExtensions`, and `ActivityTestFixtureHttpExtensions` into `Elsa.Testing.Shared`.
- Updated namespaces and imports across unit tests to reflect new structure.
- Enhanced `AssertActivityAttributes` and added fluent configuration APIs.
- Adjusted `Directory.Packages.props` with new dependencies, including `NSubstitute` and `xunit.assert`.

* Refactor `SetVariableTests`: inline `ActivityTestFixture` initialization to simplify test setup.

* Refactor `ActivityTestFixture`: eliminate redundant field `_services`, add `UsedImplicitly` attributes, and improve service collection management

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for `ActivityExecutionContextExtensions`, detailing methods and parameters.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add XML documentation for scheduler strategies and their methods

- Updated `IActivityExecutionContextSchedulerStrategy` and `IWorkflowExecutionContextSchedulerStrategy` interfaces with XML summaries for methods.
- Added XML documentation to implementations (`ActivityExecutionContextSchedulerStrategy`, `WorkflowExecutionContextSchedulerStrategy`) and test fakes for clarity.

* Refactor `WriteLineTests`: consolidate duplicate test logic, simplify setup with shared helper method, and enhance readability in assertions.

* Refactor `WriteLineTests` and `SetVariableTests`: replace `WriteLineAsync` assertions with synchronous `WriteLine`, streamline exception recording in `SetVariableTests`, and remove unused imports.

* Add new unit and integration tests for activity input and expression evaluation

- Introduced comprehensive test suites covering activity input evaluation, expression handling, and fault scenarios.
- Added unit tests for `ExpressionDescriptorRegistry`, `ExpressionEvaluator`, and activity execution context extensions.
- Added integration tests: `CustomInputEvaluatorTests`, `InputEvaluationErrorTests`, and `InputPropertyEvaluationTests`.
- Enhanced code coverage for edge cases and async evaluation logic.

* Remove unused `RunWorkflowAsync` extension method from `RunActivityExtensions`.

* Rename `CustomInputEvaluatorTests` to `InputEvaluationTests` for consistency with naming conventions.

* Remove unused `Elsa.Workflows.Activities` import from `RunActivityExtensions`.

* Remove redundant comments from `InputEvaluationErrorTests` for clarity.

* Refactor unit tests to streamline activity and expression evaluations

- Refactored `ExecuteActivityAsync` and `ExecuteWriteLineAsync` into shared helpers for consistency and reuse across tests.
- Replaced redundant mock setups with helper methods in `ExpressionDescriptorRegistryTests`.
- Simplified test setup for expression and activity evaluation by removing unused imports and consolidating configuration logic.
- Enhanced readability by reducing duplicate code and leveraging shared utility methods.

* Refactor activity input evaluation tests

- Extracted `CreateContextAsync` helper into `EvaluationTestHelpers` for reuse across evaluation test suites.
- Replaced inline activity context setup with shared helper in `InputPropertyEvaluationTests`, `WrappedInputEvaluationTests`, and related test suites.
- Simplified test method names for clarity and consistency.
- Updated test annotations to enhance readability and align with naming conventions.

* Remove redundant test cases and unused imports

- Deleted duplicated and non-essential test cases across evaluation test suites.
- Removed unused imports to improve code cleanliness and readability.
- Streamlined variable initializations and method calls within test setups.

* Remove redundant test case from `InputEvaluationErrorTests`

- Deleted the `ContinuesEvaluationForMultipleInputs` test, as it overlaps with existing tests and does not provide additional coverage.

* Remove redundant assertion from `InputPropertyEvaluationTests`

- Deleted `Assert.True(context.GetHasEvaluatedProperties())`, as it is unnecessary for verifying test outcomes.

* Remove redundant test cases from `WrappedInputEvaluationTests`

- Deleted `UsesDefaultValueWhenInputIsNull` and `EvaluatesExpression` tests as they are either duplicated or unnecessary for current test coverage.

* Add unit test projects for `Elsa.Workflows.Management` and `Elsa.Expressions`

- Introduced new test projects to separate and organize unit tests for `Elsa.Workflows.Management` and `Elsa.Expressions`.
- Updated `Elsa.sln` to include references to the newly added test projects.
- Adjusted namespaces in affected test classes for consistency with the updated project structure.

* Refactor `ExpressionEvaluatorTests` for clarity and consistency

- Simplified test method names and annotations for improved readability.
- Replaced duplicate mock setups with helper functions (`CreateContextAsync`, `CreateContextWithMockHandlerAsync`, and related methods).
- Streamlined test setups by removing redundant code and consolidating context creation logic.
- Updated test annotations to include descriptive `DisplayName` attributes.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove redundant comment from `ExpressionEvaluatorTests` for clarity

* Remove redundant blank lines from unit test classes

- Eliminated unnecessary blank lines across `ActivityExecutionContextExtensions` test suites to improve code readability and consistency.

* Update src/modules/Elsa.Expressions/Services/ExpressionEvaluator.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Expand `test-guidelines.md` with testing best practices, helper references, and example snippets

- Added detailed guidance on test project organization, updated helper documentation, and streamlined example code for activity unit testing.
- Introduced scheduler strategy information and integration test patterns for deterministic tests.
- Clarified usage of shared infrastructure like `ActivityTestFixture` and `AsyncWorkflowRunner`.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix merge issue

* Add missing `using Elsa.Expressions.Models` directive to `SetVariableTests`

* Update `SetVariableTests` and `ActivityTestFixture` to fix exception type assertion and improve test utility execution handling.

* Update `SetVariableTests` and `ActivityTestFixture` to handle null variables, fix exception type assertion, and simplify context usage.

---------

Co-authored-by: lucas.hipolito <lukhipolito@yahoo.com.br>
Co-authored-by: lukhipolito-nexxbiz <lucas.hipolito@nexxbiz.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2025-10-22 20:54:58 +02:00 committed by GitHub
parent 5c655e0a44
commit 762f23dbc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1252 additions and 22 deletions

View file

@ -195,7 +195,6 @@ public async Task Should_Return_Default_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.

View file

@ -128,12 +128,15 @@ public class ActivityTestFixture
{
var activityType = activity.GetType();
var variableProperties = activityType.GetProperties()
.Where(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>))
.Where(p => typeof(Variable).IsAssignableFrom(p.PropertyType))
.ToList();
foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!))
foreach (var variable in variableProperties.Select(property => (Variable?)property.GetValue(activity)))
{
if(variable == null)
continue;
context.WorkflowExecutionContext.MemoryRegister.Declare(variable);
variable.Set(context.ExpressionExecutionContext, variable.Value);
}

View file

@ -14,9 +14,8 @@ namespace Elsa.Workflows.Activities.Flowchart.Activities;
/// Note that this activity is no longer necessary for either AND or OR merges, because all activities inherit the Join Kind property.
/// Use this activity if an explicit join step is desired.
/// </summary>
[Activity("Elsa", "Branching", "[Obsolete] - Explicitly merge multiple branches into a single branch of execution.", DisplayName = "Join")]
[Activity("Elsa", "Branching", "Explicitly merge multiple branches into a single branch of execution.", DisplayName = "Join")]
[UsedImplicitly]
[Obsolete("Each activity now supports the MergeMode property, making the use of this activity obsolete.", false)]
public class FlowJoin : Activity, IJoinNode
{
/// <inheritdoc />
@ -38,8 +37,9 @@ public class FlowJoin : Activity, IJoinNode
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
if(!Flowchart.UseTokenFlow)
await context.ParentActivityExecutionContext.CancelInboundAncestorsAsync(this);
if (context.ParentActivityExecutionContext != null)
await context.ParentActivityExecutionContext.CancelInboundAncestorsAsync(this);
await context.CompleteActivityAsync();
}

View file

@ -8,8 +8,16 @@ public static class ActivityExtensions
{
public static MergeMode? GetMergeMode(this IActivity activity)
{
activity.CustomProperties.TryGetValue("mergeMode", out var mergeModeString);
return Enum.TryParse<MergeMode>((string?)mergeModeString, true, out var mergeMode) ? mergeMode : null;
if (!activity.CustomProperties.TryGetValue("mergeMode", out var value))
return null;
// Handle both string and enum values for backwards compatibility
return value switch
{
MergeMode mode => mode,
string str when Enum.TryParse<MergeMode>(str, true, out var mode) => mode,
_ => null
};
}
public static void SetMergeMode(this IActivity activity, MergeMode? value)
@ -17,7 +25,7 @@ public static class ActivityExtensions
if (value == null)
activity.CustomProperties.Remove("mergeMode");
else
activity.CustomProperties["mergeMode"] = value;
activity.CustomProperties["mergeMode"] = value.ToString()!;
}
public static async Task<MergeMode?> GetMergeModeAsync(this IActivity activity, ActivityExecutionContext context)

View file

@ -0,0 +1,353 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
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.Flow;
/// <summary>
/// Integration tests for counter-based flowchart execution strategy.
/// </summary>
[Collection("FlowchartTests")]
public class FlowchartCounterBasedTests : IDisposable
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output;
private readonly bool _originalFlowMode;
public FlowchartCounterBasedTests(ITestOutputHelper testOutputHelper)
{
_output = new();
_services = CreateServiceProvider(testOutputHelper, _output);
_originalFlowMode = Flowchart.UseTokenFlow;
Flowchart.UseTokenFlow = false;
}
public void Dispose()
{
Flowchart.UseTokenFlow = _originalFlowMode;
}
[Fact(DisplayName = "Executes simple linear flowchart")]
public async Task ExecutesSimpleLinearFlowchart()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("First"),
new WriteLine("Second"),
new WriteLine("Third")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Equal("First", _output.Lines.ElementAt(0));
Assert.Equal("Second", _output.Lines.ElementAt(1));
Assert.Equal("Third", _output.Lines.ElementAt(2));
}
[Fact(DisplayName = "Executes both branches in parallel flowchart")]
public async Task ExecutesBothBranches()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
}
[Fact(DisplayName = "Handles flowchart with no connections")]
public async Task HandlesNoConnections()
{
// Arrange
var activity = new WriteLine("Isolated");
var flowchart = new Flowchart
{
Start = activity,
Activities = { activity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Isolated", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Completes when start activity is null")]
public async Task CompletesWhenStartIsNull()
{
// Arrange
var flowchart = new Flowchart
{
Start = null
};
// Act
var result = await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.NotNull(result);
Assert.Empty(_output.Lines);
}
[Fact(DisplayName = "Follows conditional branches with If activity")]
public async Task FollowsConditionalBranches()
{
// Arrange
var ifActivity = new If
{
Condition = new(true),
Then = new WriteLine("Then branch"),
Else = new WriteLine("Else branch")
};
var flowchart = new Flowchart
{
Start = ifActivity,
Activities = { ifActivity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Then branch", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Executes join node with WaitAny mode")]
public async Task ExecutesJoinNodeWaitAny()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
var afterJoin = new WriteLine("AfterJoin");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, join, afterJoin },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, join),
CreateConnection(branch2, join),
CreateConnection(join, afterJoin)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("AfterJoin", _output.Lines);
// At least one branch should execute
Assert.True(_output.Lines.Contains("Branch1") || _output.Lines.Contains("Branch2"));
}
[Fact(DisplayName = "Executes join node with WaitAll mode")]
public async Task ExecutesJoinNodeWaitAll()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
var afterJoin = new WriteLine("AfterJoin");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, join, afterJoin },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, join),
CreateConnection(branch2, join),
CreateConnection(join, afterJoin)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
Assert.Contains("AfterJoin", _output.Lines);
}
[Fact(DisplayName = "Handles multiple sequential joins")]
public async Task HandlesMultipleSequentialJoins()
{
// Arrange
var start = new WriteLine("Start");
var a1 = new WriteLine("A1");
var a2 = new WriteLine("A2");
var join1 = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
var b1 = new WriteLine("B1");
var b2 = new WriteLine("B2");
var join2 = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, a1, a2, join1, b1, b2, join2, end },
Connections =
{
CreateConnection(start, a1),
CreateConnection(start, a2),
CreateConnection(a1, join1),
CreateConnection(a2, join1),
CreateConnection(join1, b1),
CreateConnection(join1, b2),
CreateConnection(b1, join2),
CreateConnection(b2, join2),
CreateConnection(join2, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("A1", _output.Lines);
Assert.Contains("A2", _output.Lines);
Assert.Contains("B1", _output.Lines);
Assert.Contains("B2", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Handles complex diamond pattern")]
public async Task HandlesComplexDiamondPattern()
{
// Arrange
var start = new WriteLine("Start");
var left1 = new WriteLine("Left1");
var left2 = new WriteLine("Left2");
var right1 = new WriteLine("Right1");
var right2 = new WriteLine("Right2");
var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAll) };
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, left1, left2, right1, right2, join, end },
Connections =
{
CreateConnection(start, left1),
CreateConnection(start, right1),
CreateConnection(left1, left2),
CreateConnection(right1, right2),
CreateConnection(left2, join),
CreateConnection(right2, join),
CreateConnection(join, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Left1", _output.Lines);
Assert.Contains("Left2", _output.Lines);
Assert.Contains("Right1", _output.Lines);
Assert.Contains("Right2", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Executes activities in correct order for sequential flow")]
public async Task ExecutesInCorrectOrderForSequential()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("1"),
new WriteLine("2"),
new WriteLine("3"),
new WriteLine("4")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(4, _output.Lines.Count);
Assert.Equal("1", _output.Lines.ElementAt(0));
Assert.Equal("2", _output.Lines.ElementAt(1));
Assert.Equal("3", _output.Lines.ElementAt(2));
Assert.Equal("4", _output.Lines.ElementAt(3));
}
[Fact(DisplayName = "Handles nested flowcharts")]
public async Task HandlesNestedFlowcharts()
{
// Arrange
var innerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Inner1"),
new WriteLine("Inner2")
);
var outerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Outer1"),
innerFlowchart,
new WriteLine("Outer2")
);
// Act
await RunFlowchartAsync(_services, outerFlowchart);
// Assert
Assert.Contains("Outer1", _output.Lines);
Assert.Contains("Inner1", _output.Lines);
Assert.Contains("Inner2", _output.Lines);
Assert.Contains("Outer2", _output.Lines);
}
[Fact(DisplayName = "Handles unconnected activities in flowchart")]
public async Task HandlesUnconnectedActivities()
{
// Arrange
var connected = new WriteLine("Connected");
var unconnected = new WriteLine("Unconnected");
var flowchart = new Flowchart
{
Start = connected,
Activities = { connected, unconnected }
// No connection to unconnected activity
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Connected", _output.Lines.ElementAt(0));
Assert.DoesNotContain("Unconnected", _output.Lines);
}
}

View file

@ -0,0 +1,10 @@
namespace Elsa.Activities.IntegrationTests.Flow;
/// <summary>
/// Defines a test collection to ensure flowchart tests don't run in parallel.
/// This is necessary because the tests modify the process-wide static Flowchart.UseTokenFlow flag.
/// </summary>
[CollectionDefinition("FlowchartTests", DisableParallelization = true)]
public class FlowchartTestCollection
{
}

View file

@ -0,0 +1,69 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Models;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests.Flow;
/// <summary>
/// Shared helper methods for Flowchart integration tests.
/// </summary>
public static class FlowchartTestHelpers
{
public static IServiceProvider CreateServiceProvider(ITestOutputHelper testOutputHelper, CapturingTextWriter? capturingTextWriter = null)
{
var builder = new TestApplicationBuilder(testOutputHelper);
if (capturingTextWriter != null)
builder.WithCapturingTextWriter(capturingTextWriter);
return builder.Build();
}
public static async Task<RunWorkflowResult> RunFlowchartAsync(IServiceProvider services, Flowchart flowchart)
{
return await services.RunActivityAsync(flowchart);
}
public static Connection CreateConnection(IActivity source, IActivity target, string? outcome = "Done")
{
return new(new(source, outcome), new Endpoint(target));
}
public static Flowchart CreateSimpleLinearFlowchart(params IActivity[] activities)
{
var flowchart = new Flowchart
{
Start = activities.FirstOrDefault(),
Activities = new List<IActivity>(activities)
};
for (var i = 0; i < activities.Length - 1; i++)
{
flowchart.Connections.Add(CreateConnection(activities[i], activities[i + 1]));
}
return flowchart;
}
public static Flowchart CreateBranchingFlowchart(IActivity start, IActivity branch1, IActivity branch2, IActivity? join = null)
{
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2 }
};
flowchart.Connections.Add(CreateConnection(start, branch1));
flowchart.Connections.Add(CreateConnection(start, branch2));
if (join == null)
return flowchart;
flowchart.Activities.Add(join);
flowchart.Connections.Add(CreateConnection(branch1, join));
flowchart.Connections.Add(CreateConnection(branch2, join));
return flowchart;
}
}

View file

@ -0,0 +1,549 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Xunit.Abstractions;
using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers;
namespace Elsa.Activities.IntegrationTests.Flow;
/// <summary>
/// Integration tests for token-based flowchart execution strategy.
/// </summary>
[Collection("FlowchartTests")]
public class FlowchartTokenBasedTests : IDisposable
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output;
private readonly bool _originalFlowMode;
public FlowchartTokenBasedTests(ITestOutputHelper testOutputHelper)
{
_output = new();
_services = CreateServiceProvider(testOutputHelper, _output);
_originalFlowMode = Flowchart.UseTokenFlow;
Flowchart.UseTokenFlow = true;
}
public void Dispose()
{
Flowchart.UseTokenFlow = _originalFlowMode;
}
[Fact(DisplayName = "Executes simple linear flowchart")]
public async Task ExecutesSimpleLinearFlowchart()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("First"),
new WriteLine("Second"),
new WriteLine("Third")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Equal("First", _output.Lines.ElementAt(0));
Assert.Equal("Second", _output.Lines.ElementAt(1));
Assert.Equal("Third", _output.Lines.ElementAt(2));
}
[Fact(DisplayName = "Executes both branches in parallel flowchart")]
public async Task ExecutesBothBranches()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
}
[Fact(DisplayName = "Handles flowchart with no connections")]
public async Task HandlesNoConnections()
{
// Arrange
var activity = new WriteLine("Isolated");
var flowchart = new Flowchart
{
Start = activity,
Activities = { activity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Isolated", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Completes when start activity is null")]
public async Task CompletesWhenStartIsNull()
{
// Arrange
var flowchart = new Flowchart
{
Start = null
};
// Act
var result = await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.NotNull(result);
Assert.Empty(_output.Lines);
}
[Fact(DisplayName = "Follows conditional branches with If activity")]
public async Task FollowsConditionalBranches()
{
// Arrange
var ifActivity = new If
{
Condition = new(true),
Then = new WriteLine("Then branch"),
Else = new WriteLine("Else branch")
};
var flowchart = new Flowchart
{
Start = ifActivity,
Activities = { ifActivity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Then branch", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Executes Stream merge mode - schedules immediately")]
public async Task ExecutesStreamMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var afterJoin = new WriteLine("AfterJoin");
afterJoin.SetMergeMode(MergeMode.Stream);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, afterJoin },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, afterJoin),
CreateConnection(branch2, afterJoin)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("AfterJoin", _output.Lines);
// In Stream mode, afterJoin executes as soon as first branch arrives
}
[Fact(DisplayName = "Executes Race merge mode - cancels other branches")]
public async Task ExecutesRaceMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var afterRace = new WriteLine("AfterRace");
afterRace.SetMergeMode(MergeMode.Race);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, afterRace },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, afterRace),
CreateConnection(branch2, afterRace)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("AfterRace", _output.Lines);
// In Race mode, afterRace executes on first arrival and blocks others
}
[Fact(DisplayName = "Executes Converge merge mode - waits for all branches")]
public async Task ExecutesConvergeMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, converge, end },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, converge),
CreateConnection(branch2, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Executes None merge mode correctly")]
public async Task ExecutesNoneMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var noneMode = new WriteLine("NoneMode");
noneMode.SetMergeMode(MergeMode.None);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, noneMode },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, noneMode),
CreateConnection(branch2, noneMode)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
Assert.Contains("NoneMode", _output.Lines);
}
[Fact(DisplayName = "Handles token consumption correctly")]
public async Task HandlesTokenConsumption()
{
// Arrange
var start = new WriteLine("Start");
var middle = new WriteLine("Middle");
var end = new WriteLine("End");
var flowchart = CreateSimpleLinearFlowchart(start, middle, end);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
// Tokens should be consumed after each activity completes
Assert.Equal(3, _output.Lines.Count);
Assert.Equal("Start", _output.Lines.ElementAt(0));
Assert.Equal("Middle", _output.Lines.ElementAt(1));
Assert.Equal("End", _output.Lines.ElementAt(2));
}
[Fact(DisplayName = "Handles multiple sequential converge nodes")]
public async Task HandlesMultipleSequentialConvergeNodes()
{
// Arrange
var start = new WriteLine("Start");
var a1 = new WriteLine("A1");
var a2 = new WriteLine("A2");
var converge1 = new WriteLine("Converge1");
converge1.SetMergeMode(MergeMode.Converge);
var b1 = new WriteLine("B1");
var b2 = new WriteLine("B2");
var converge2 = new WriteLine("Converge2");
converge2.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, a1, a2, converge1, b1, b2, converge2, end },
Connections =
{
CreateConnection(start, a1),
CreateConnection(start, a2),
CreateConnection(a1, converge1),
CreateConnection(a2, converge1),
CreateConnection(converge1, b1),
CreateConnection(converge1, b2),
CreateConnection(b1, converge2),
CreateConnection(b2, converge2),
CreateConnection(converge2, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("A1", _output.Lines);
Assert.Contains("A2", _output.Lines);
Assert.Contains("Converge1", _output.Lines);
Assert.Contains("B1", _output.Lines);
Assert.Contains("B2", _output.Lines);
Assert.Contains("Converge2", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Handles complex diamond pattern with tokens")]
public async Task HandlesComplexDiamondPattern()
{
// Arrange
var start = new WriteLine("Start");
var left1 = new WriteLine("Left1");
var left2 = new WriteLine("Left2");
var right1 = new WriteLine("Right1");
var right2 = new WriteLine("Right2");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, left1, left2, right1, right2, converge, end },
Connections =
{
CreateConnection(start, left1),
CreateConnection(start, right1),
CreateConnection(left1, left2),
CreateConnection(right1, right2),
CreateConnection(left2, converge),
CreateConnection(right2, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Left1", _output.Lines);
Assert.Contains("Left2", _output.Lines);
Assert.Contains("Right1", _output.Lines);
Assert.Contains("Right2", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Executes activities in correct order for sequential flow")]
public async Task ExecutesInCorrectOrderForSequential()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("1"),
new WriteLine("2"),
new WriteLine("3"),
new WriteLine("4")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(4, _output.Lines.Count);
Assert.Equal("1", _output.Lines.ElementAt(0));
Assert.Equal("2", _output.Lines.ElementAt(1));
Assert.Equal("3", _output.Lines.ElementAt(2));
Assert.Equal("4", _output.Lines.ElementAt(3));
}
[Fact(DisplayName = "Handles nested flowcharts with tokens")]
public async Task HandlesNestedFlowcharts()
{
// Arrange
var innerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Inner1"),
new WriteLine("Inner2")
);
var outerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Outer1"),
innerFlowchart,
new WriteLine("Outer2")
);
// Act
await RunFlowchartAsync(_services, outerFlowchart);
// Assert
Assert.Contains("Outer1", _output.Lines);
Assert.Contains("Inner1", _output.Lines);
Assert.Contains("Inner2", _output.Lines);
Assert.Contains("Outer2", _output.Lines);
}
[Fact(DisplayName = "Handles unconnected activities in flowchart")]
public async Task HandlesUnconnectedActivities()
{
// Arrange
var connected = new WriteLine("Connected");
var unconnected = new WriteLine("Unconnected");
var flowchart = new Flowchart
{
Start = connected,
Activities = { connected, unconnected }
// No connection to unconnected activity
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Connected", _output.Lines.ElementAt(0));
Assert.DoesNotContain("Unconnected", _output.Lines);
}
[Fact(DisplayName = "Handles mixed merge modes in complex flow")]
public async Task HandlesMixedMergeModes()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var stream = new WriteLine("Stream");
stream.SetMergeMode(MergeMode.Stream);
var branch3 = new WriteLine("Branch3");
var branch4 = new WriteLine("Branch4");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, stream, branch3, branch4, converge, end },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, stream),
CreateConnection(branch2, stream),
CreateConnection(stream, branch3),
CreateConnection(stream, branch4),
CreateConnection(branch3, converge),
CreateConnection(branch4, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Stream", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Handles converge with single inbound connection")]
public async Task HandlesConvergeWithSingleInbound()
{
// Arrange
var start = new WriteLine("Start");
var single = new WriteLine("Single");
single.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = CreateSimpleLinearFlowchart(start, single, end);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Single", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Emits and consumes tokens correctly across multiple steps")]
public async Task EmitsAndConsumesTokensCorrectly()
{
// Arrange
var step1 = new WriteLine("Step1");
var step2a = new WriteLine("Step2a");
var step2b = new WriteLine("Step2b");
var step3 = new WriteLine("Step3");
step3.SetMergeMode(MergeMode.Converge);
var step4 = new WriteLine("Step4");
var flowchart = new Flowchart
{
Start = step1,
Activities = { step1, step2a, step2b, step3, step4 },
Connections =
{
CreateConnection(step1, step2a),
CreateConnection(step1, step2b),
CreateConnection(step2a, step3),
CreateConnection(step2b, step3),
CreateConnection(step3, step4)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
// Verify all activities executed in a valid order
Assert.Contains("Step1", _output.Lines);
Assert.Contains("Step2a", _output.Lines);
Assert.Contains("Step2b", _output.Lines);
Assert.Contains("Step3", _output.Lines);
Assert.Contains("Step4", _output.Lines);
// Step3 should only appear once (tokens consumed properly)
Assert.Single(_output.Lines, l => l == "Step3");
}
}

View file

@ -0,0 +1,63 @@
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests;
public class SetVariableTests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public SetVariableTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "SetVariable sets variable in nearest scope when multiple variables with same name exist")]
public async Task SetVariable_SetsVariableInNearestScope_WhenMultipleVariablesWithSameNameExist()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<VariableScopingWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
// The sequence-level variable should be set to "Sequence Value"
Assert.Equal(new[] { "Sequence Value" }, lines);
}
}
class VariableScopingWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var workflowLevelVariable = new Variable<string>("Foo", "Workflow Value");
var sequenceLevelVariable = new Variable<string>("Foo", "Initial Value");
workflow.Root = new Sequence
{
Variables = { workflowLevelVariable },
Activities =
{
new Sequence
{
Variables = { sequenceLevelVariable },
Activities =
{
new SetVariable
{
Variable = sequenceLevelVariable,
Value = new("Sequence Value")
},
new WriteLine(context => context.GetVariable<string>("Foo"))
}
}
}
};
}
}

View file

@ -0,0 +1,17 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Activities;
namespace Elsa.Activities.UnitTests.Flow;
/// <summary>
/// Shared helper methods for Flowchart unit tests.
/// </summary>
public static class FlowchartTestHelpers
{
public static async Task<ActivityExecutionContext> ExecuteFlowchartAsync(Flowchart flowchart)
{
var fixture = new ActivityTestFixture(flowchart);
return await fixture.ExecuteAsync();
}
}

View file

@ -0,0 +1,93 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities.Flowchart.Activities;
using static Elsa.Activities.UnitTests.Flow.FlowchartTestHelpers;
namespace Elsa.Activities.UnitTests.Flow;
/// <summary>
/// Tests for common Flowchart behavior (both counter and token-based strategies).
/// </summary>
public class FlowchartTests
{
[Fact(DisplayName = "Schedules start activity when specified")]
public async Task SchedulesStartActivity()
{
// Arrange
var startActivity = new WriteLine("Start");
var flowchart = new Flowchart
{
Start = startActivity
};
// Act
var context = await ExecuteFlowchartAsync(flowchart);
// Assert
Assert.True(context.HasScheduledActivity(startActivity));
}
[Fact(DisplayName = "Executes without error when no start activity specified")]
public async Task ExecutesWithoutErrorWhenNoStartActivity()
{
// Arrange
var flowchart = new Flowchart
{
Start = null
};
// Act
var context = await ExecuteFlowchartAsync(flowchart);
// Assert
Assert.NotNull(context);
Assert.False(context.HasScheduledActivity(new WriteLine("NonExistent")));
}
[Theory(DisplayName = "Respects UseTokenFlow flag")]
[InlineData(true)]
[InlineData(false)]
public async Task RespectsUseTokenFlowFlag(bool useTokenFlow)
{
// Arrange
var originalValue = Flowchart.UseTokenFlow;
Flowchart.UseTokenFlow = useTokenFlow;
try
{
var activity = new WriteLine("Test");
var flowchart = new Flowchart
{
Start = activity,
Activities = { activity }
};
// Act
var context = await ExecuteFlowchartAsync(flowchart);
// Assert - just verify it executes without error
Assert.NotNull(context);
}
finally
{
Flowchart.UseTokenFlow = originalValue;
}
}
[Fact(DisplayName = "Accepts empty connections collection")]
public async Task AcceptsEmptyConnections()
{
// Arrange
var activity = new WriteLine("Isolated");
var flowchart = new Flowchart
{
Start = activity,
Activities = { activity }
};
// Act
var context = await ExecuteFlowchartAsync(flowchart);
// Assert
Assert.True(context.HasScheduledActivity(activity));
}
}

View file

@ -285,4 +285,4 @@ public class SendHttpRequestTests
{
return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!);
}
}
}

View file

@ -0,0 +1,51 @@
using Elsa.Testing.Shared;
namespace Elsa.Activities.UnitTests.Primitives;
public class SetVariableOfTTests
{
[Fact]
public async Task Should_Set_Variable_Integer()
{
// Arrange
const int expected = 42; // The answer to life, the universe and everything.
var variable = new Variable<int>("myVar", 0, "myVar");
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
// Act
var fixture = new ActivityTestFixture(setVariable);
var context = await fixture.ExecuteAsync();
// Assert
var result = variable.Get(context);
Assert.Equal(expected, result);
}
[Fact]
public async Task Should_Throw_When_Variable_Is_Null()
{
// Arrange
var setVariable = new SetVariable<string>(null!, new Input<string>("test value"));
// Act & Assert
var exception = await Record.ExceptionAsync(() => new ActivityTestFixture(setVariable).ExecuteAsync());
Assert.NotNull(exception);
}
[Fact]
public async Task Should_Set_Variable_To_Null_Value()
{
// Arrange
var variable = new Variable<string?>("myVar", "initial value", "myVar");
var setVariable = new SetVariable<string?>(variable, new Input<string?>((string?)null));
// Act
var fixture = new ActivityTestFixture(setVariable);
var context = await fixture.ExecuteAsync();
// Assert
var result = variable.Get(context);
Assert.Null(result);
}
}

View file

@ -1,4 +1,6 @@
using Elsa.Testing.Shared;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows;
namespace Elsa.Activities.UnitTests.Primitives;
@ -6,12 +8,16 @@ namespace Elsa.Activities.UnitTests.Primitives;
public class SetVariableTests
{
[Fact]
public async Task Should_Set_Variable_Integer()
public async Task Should_Set_Variable()
{
// Arrange
const int expected = 42; // The answer to life, the universe and everything.
var variable = new Variable<int>("myVar", 0, "myVar");
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
const int expected = 42;
var variable = new Variable("myVar", 0, "myVar");
var setVariable = new SetVariable
{
Variable = variable,
Value = new(expected)
};
// Act
var context = await ExecuteAsync(setVariable);
@ -25,12 +31,17 @@ public class SetVariableTests
public async Task Should_Throw_When_Variable_Is_Null()
{
// Arrange
var setVariable = new SetVariable<string>(null!, new Input<string>("test value"));
var setVariable = new SetVariable
{
Variable = null,
Value = new("test value")
};
// Act & Assert
var exception = await Record.ExceptionAsync(() => ExecuteAsync(setVariable));
Assert.NotNull(exception);
Assert.IsType<InvalidOperationException>(exception);
}
[Fact]
@ -38,7 +49,11 @@ public class SetVariableTests
{
// Arrange
var variable = new Variable<string?>("myVar", "initial value", "myVar");
var setVariable = new SetVariable<string?>(variable, new Input<string?>((string?)null));
var setVariable = new SetVariable
{
Variable = variable,
Value = new(new Literal(null))
};
// Act
var context = await ExecuteAsync(setVariable);
@ -47,9 +62,9 @@ public class SetVariableTests
var result = variable.Get(context);
Assert.Null(result);
}
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
private static async Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
{
return new ActivityTestFixture(activity).ExecuteAsync();
return await new ActivityTestFixture(activity).ExecuteAsync();
}
}