Add integration tests for WorkflowDefinitionActivity scenarios (#6999)

* 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`.

* Add unit tests for `FlowSendHttpRequest` activity and refactor helper utilities

- Introduce `FlowSendHttpRequestTests` to validate activity behavior across various scenarios.
- Add utility methods in `SendHttpRequestTestHelpers` for creating mock responses and exceptions.
- Refactor `Helpers` namespace to `Http.Helpers` for better organization.
- Update project file to reflect namespace adjustments.

* Update test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj

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

* Add integration tests for WorkflowDefinitionActivity scenarios

- Introduce integration tests covering inputs, outputs, version fallback, and outcome handling.
- Add JSON workflow definitions to validate parent-child workflow execution.
- Update project files to include new test scenarios.
- Enhance error handling in `RunWorkflowExtensions` for missing workflow definitions.
- Adjust JavaScript expression handler to support PascalCase activity names.

* Refactor WorkflowDefinitionActivity tests to use Theory with dynamic test cases

- Consolidate multiple Fact-based tests into a single parameterized Theory.
- Introduce `WorkflowDefinitionActivityTestCases` for reusable test scenarios.
- Simplify assertions and improve test structure for better maintainability.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2025-10-21 15:37:10 +02:00 committed by GitHub
parent 84c04341f9
commit 66543a48b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 709 additions and 4 deletions

View file

@ -35,11 +35,15 @@ public static class RunWorkflowExtensions
{
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published);
if (workflowGraph == null)
throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found.");
var workflowRuntime = services.GetRequiredService<IWorkflowRuntime>();
var workflowClient = await workflowRuntime.CreateClientAsync();
var response = await workflowClient.CreateAndRunInstanceAsync(new()
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph!.Workflow.Identity.Id),
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id),
Input = input
});

View file

@ -75,6 +75,6 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions<JintOp
await foreach (var activityOutput in activityOutputs)
foreach (var outputName in activityOutput.OutputNames.FilterInvalidVariableNames())
engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
engine.SetValue($"get{outputName}From{activityOutput.ActivityName.Pascalize()}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
}
}

View file

@ -35,5 +35,32 @@
<None Update="Scenarios\JoinBehaviors\Workflows\decision-merge-join-none.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-input.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-input.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-outcomes.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-output.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-version-fallback.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-io.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-outcomes.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-output.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-published-version.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -17,12 +17,13 @@ public class Tests
}
[Fact(DisplayName = "Workflows provided from JSON files that depend on other workflows are executed correctly.")]
public async Task Test1()
public async Task Should_Execute_Multi_Level_Nested_Workflow_Definitions()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Run the "matter" workflow.
// Run the "matter" workflow which contains MoleculeActivity, which contains AtomActivity.
// Matter -> 3x Molecule -> each Molecule contains 3x Atom = 9 "Atom" outputs total
await _services.RunWorkflowUntilEndAsync("matter-workflow");
// Assert.

View file

@ -0,0 +1,41 @@
using Elsa.Testing.Shared;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionActivities;
public class Tests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public Tests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.WithWorkflowsFromDirectory("Scenarios", "WorkflowDefinitionActivities", "Workflows")
.Build();
}
[Theory]
[MemberData(nameof(WorkflowDefinitionActivityTestCases))]
public async Task Should_Execute_WorkflowDefinitionActivity_Scenarios(string workflowDefinitionId, string expectedOutput)
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Run the workflow.
await _services.RunWorkflowUntilEndAsync(workflowDefinitionId);
// Assert - verify expected output appears.
var lines = _capturingTextWriter.Lines.ToList();
Assert.Contains(expectedOutput, lines);
}
public static IEnumerable<object[]> WorkflowDefinitionActivityTestCases()
{
yield return ["parent-with-input", "Hello from parent!"];
yield return ["parent-with-output", "Received from child: Child output value"];
yield return ["parent-with-outcomes", "Success path taken"];
yield return ["parent-version-fallback", "Published version executed"];
}
}

View file

@ -0,0 +1,23 @@
{
"id": "child-published-v1",
"definitionId": "child-published",
"name": "ChildPublished",
"version": 1,
"isLatest": true,
"isPublished": true,
"usableAsActivity": true,
"inputs": [],
"outputs": [],
"root": {
"type": "Elsa.WriteLine",
"version": 1,
"id": "WriteLine1",
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Published version executed"
}
}
}
}

View file

@ -0,0 +1,79 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "e694d38a6d17f6ab",
"definitionId": "d0ae88e4c15eacc0",
"name": "ChildWithInput",
"createdAt": "2025-10-21T12:06:12.52497\u002B00:00",
"version": 1,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [
{
"uiHint": "singleline",
"storageDriverType": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core",
"type": "String",
"name": "Message",
"displayName": "Message",
"description": "The message to write",
"category": "Primitives"
}
],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"id": "855226c76e9ad551",
"nodeId": "Workflow1:855226c76e9ad551",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "getMessage()"
}
},
"id": "eff084a902159510",
"nodeId": "Workflow1:855226c76e9ad551:eff084a902159510",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -278.5,
"y": -178.5
},
"size": {
"width": 159.6171875,
"height": 67.9765625
}
}
}
}
],
"variables": [],
"connections": []
}
}

View file

@ -0,0 +1,72 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "85761829037332b7",
"definitionId": "7ee5c04453217e3f",
"name": "ChildWithOutcomes",
"createdAt": "2025-10-21T13:11:59.899105\u002B00:00",
"version": 2,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [
"Success",
"Failed"
],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"id": "3519519e32e50f38",
"nodeId": "Workflow1:3519519e32e50f38",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"outcomes": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Success"
}
},
"id": "a093b294de68dc32",
"nodeId": "Workflow1:3519519e32e50f38:a093b294de68dc32",
"name": "Complete1",
"type": "Elsa.Complete",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -160.5,
"y": -143.5
},
"size": {
"width": 157.15625,
"height": 67.9765625
}
}
}
}
],
"variables": [],
"connections": []
}
}

View file

@ -0,0 +1,84 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "4da8180d0c9f7e3f",
"definitionId": "5781ed56ea4b160",
"name": "ChildWithOutput",
"createdAt": "2025-10-21T12:15:08.404102\u002B00:00",
"version": 4,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [],
"outputs": [
{
"type": "String",
"name": "Output",
"displayName": "Output",
"description": "The result value",
"category": "Primitives"
}
],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"id": "14493561a73301af",
"nodeId": "Workflow1:14493561a73301af",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"outputName": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Output"
}
},
"outputValue": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Child output value"
}
},
"id": "461b5cebad79475c",
"nodeId": "Workflow1:14493561a73301af:461b5cebad79475c",
"name": "SetOutput1",
"type": "Elsa.SetOutput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -353.5,
"y": -235.5
},
"size": {
"width": 132.3203125,
"height": 67.9765625
}
}
}
}
],
"variables": [],
"connections": []
}
}

View file

@ -0,0 +1,23 @@
{
"id": "parent-version-fallback-v1",
"definitionId": "parent-version-fallback",
"name": "ParentVersionFallback",
"version": 1,
"isLatest": true,
"isPublished": true,
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "Flowchart1",
"start": "ChildActivity1",
"activities": [
{
"type": "ChildPublished",
"version": 1,
"id": "ChildActivity1",
"workflowDefinitionId": "child-published",
"workflowDefinitionVersionId": "non-existent-version-id"
}
]
}
}

View file

@ -0,0 +1,74 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "70a093d85f846d05",
"definitionId": "parent-with-input",
"name": "ParentWithInput",
"createdAt": "2025-10-21T12:08:40.940992\u002B00:00",
"version": 4,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "Flowchart1",
"nodeId": "Workflow2:Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"workflowDefinitionId": "d0ae88e4c15eacc0",
"workflowDefinitionVersionId": "e694d38a6d17f6ab",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "e694d38a6d17f6ab",
"id": "ChildActivity1",
"nodeId": "Workflow2:Flowchart1:ChildActivity1",
"name": null,
"type": "ChildWithInput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -80,
"y": -40
},
"size": {
"width": 161.8515625,
"height": 67.9765625
}
}
},
"message": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello from parent!"
},
"memoryReference": {
"id": "ChildActivity1:input-message"
}
}
}
],
"variables": [],
"connections": []
}
}

View file

@ -0,0 +1,23 @@
{
"id": "parent-with-io-v1",
"definitionId": "parent-with-io",
"name": "ParentWithIO",
"version": 1,
"toolVersion": "3.6.0.0",
"isLatest": true,
"isPublished": true,
"root": {
"type": "ChildWithInput",
"version": 1,
"id": "ChildActivity1",
"workflowDefinitionId": "child-with-input",
"workflowDefinitionVersionId": "child-with-input-v1",
"message": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Test message"
}
}
}
}

View file

@ -0,0 +1,147 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "a09fb3bdfd316332",
"definitionId": "parent-with-outcomes",
"name": "ParentWithOutcomes",
"createdAt": "2025-10-21T13:11:28.283567\u002B00:00",
"version": 2,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "Flowchart1",
"nodeId": "Workflow2:Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"workflowDefinitionId": "7ee5c04453217e3f",
"workflowDefinitionVersionId": "85761829037332b7",
"latestAvailablePublishedVersion": 2,
"latestAvailablePublishedVersionId": "85761829037332b7",
"id": "ChildActivity1",
"nodeId": "Workflow2:Flowchart1:ChildActivity1",
"name": null,
"type": "ChildWithOutcomes",
"version": 2,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -140,
"y": 0
},
"size": {
"width": 198.7421875,
"height": 67.9765625
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Failed path taken"
}
},
"id": "WriteLineFailed",
"nodeId": "Workflow2:Flowchart1:WriteLineFailed",
"name": null,
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 140,
"y": 80
},
"size": {
"width": 159.6171875,
"height": 67.9765625
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Success path taken"
}
},
"id": "WriteLineSuccess",
"nodeId": "Workflow2:Flowchart1:WriteLineSuccess",
"name": null,
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 180,
"y": -220
},
"size": {
"width": 159.6171875,
"height": 67.9765625
}
}
}
}
],
"variables": [],
"connections": [
{
"source": {
"activity": "ChildActivity1",
"port": "Success"
},
"target": {
"activity": "WriteLineSuccess",
"port": "In"
},
"vertices": []
},
{
"source": {
"activity": "ChildActivity1",
"port": "Failed"
},
"target": {
"activity": "WriteLineFailed",
"port": "In"
},
"vertices": []
}
]
}
}

View file

@ -0,0 +1,107 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "b1f4fc14b3c6b51",
"definitionId": "parent-with-output",
"name": "ParentWithOutput",
"createdAt": "2025-10-21T12:27:50.918094\u002B00:00",
"version": 1,
"toolVersion": "3.6.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "bcbe1e9e5a9c9b0e",
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"workflowDefinitionId": "5781ed56ea4b160",
"workflowDefinitionVersionId": "4da8180d0c9f7e3f",
"latestAvailablePublishedVersion": 4,
"latestAvailablePublishedVersionId": "4da8180d0c9f7e3f",
"id": "c98383464dab2047",
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e:c98383464dab2047",
"name": "ChildWithOutput1",
"type": "ChildWithOutput",
"version": 4,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -323.5,
"y": -85.5
},
"size": {
"width": 174.1640625,
"height": 67.9765625
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "\u0060Received from child: ${getOutputFromChildWithOutput1()}\u0060"
}
},
"id": "b4f314dce11c7135",
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e:b4f314dce11c7135",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -27.5,
"y": -85.5
},
"size": {
"width": 159.6171875,
"height": 67.9765625
}
}
}
}
],
"variables": [],
"connections": [
{
"source": {
"activity": "c98383464dab2047",
"port": "Done"
},
"target": {
"activity": "b4f314dce11c7135",
"port": "In"
},
"vertices": []
}
]
}
}