Enhance WorkflowTestFixture and RunJavaScript tests with additional examples and helper methods

- Add integration test cases for validating script execution, outcomes, workflow variables, and fault handling.
- Introduce helper methods in `WorkflowTestFixture` for outcome retrieval, activity status, and output assertions.
- Extend test guidelines with usage examples for new `WorkflowTestFixture` capabilities.
This commit is contained in:
Sipke Schoorstra 2025-10-21 20:36:24 +02:00
parent 07ba8184b8
commit ad0dfad77e
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
4 changed files with 216 additions and 32 deletions

View file

@ -204,7 +204,7 @@ public async Task Should_Return_Default_Outcome()
Pattern note: [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) returns a [`RunWorkflowResult`](../../src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs) (or equivalent) containing the [`WorkflowInstance`](../../src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs) and output variables when run to completion.
Use returned state for deterministic assertions where possible.
**Example (using WorkflowTestFixture):**
**Example (using WorkflowTestFixture - basic):**
```csharp
public class RunJavaScriptTests
{
@ -215,18 +215,50 @@ public class RunJavaScriptTests
_fixture = new WorkflowTestFixture(testOutputHelper);
}
[Fact(DisplayName = "RunJavaScript should execute simple arithmetic script")]
public async Task Should_Execute_Simple_Script()
[Fact(DisplayName = "RunJavaScript should execute and return output")]
public async Task Should_Execute_And_Return_Output()
{
// Arrange
var script = "return 1 + 1;";
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
var result = await _fixture.RunActivityAsync(activity);
// Assert - workflow completes successfully
Assert.Empty(_fixture.CapturingTextWriter.Lines);
// Assert - activity produced expected output
var output = result.GetActivityOutput<object>(activity);
Assert.Equal(2, output);
}
[Fact(DisplayName = "RunJavaScript should set outcomes")]
public async Task Should_Set_Outcomes()
{
// Arrange
var script = "setOutcomes(['Branch1', 'Branch2']);";
var activity = new RunJavaScript { Script = new(script) };
// Act
var result = await _fixture.RunActivityAsync(activity);
// Assert - activity produced expected outcomes
var outcomes = _fixture.GetOutcomes(result, activity);
Assert.Contains("Branch1", outcomes);
Assert.Contains("Branch2", outcomes);
}
[Fact(DisplayName = "RunJavaScript should fault on invalid syntax")]
public async Task Should_Fault_On_Invalid_Syntax()
{
// Arrange
var script = "this is not valid javascript";
var activity = new RunJavaScript { Script = new(script) };
// Act
var result = await _fixture.RunActivityAsync(activity);
// Assert - activity should be in faulted state
var status = _fixture.GetActivityStatus(result, activity);
Assert.Equal(ActivityStatus.Faulted, status);
}
}
```
@ -301,6 +333,9 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status);
| `WorkflowTestFixture.RunActivityAsync` | Run single activity as workflow | Integration tests for single activities |
| `WorkflowTestFixture.RunWorkflowAsync` | Run workflow or workflow by definition ID | Integration tests for workflows |
| `WorkflowTestFixture.CapturingTextWriter` | Capture WriteLine output | Asserting text output in integration tests |
| `WorkflowTestFixture.GetOutcomes` | Get all outcomes from specific activity | Asserting activity outcomes in integration tests |
| `WorkflowTestFixture.HasOutcome` | Check if activity produced specific outcome | Asserting single outcome in integration tests |
| `WorkflowTestFixture.GetActivityStatus` | Get execution status of specific activity | Asserting activity status (Faulted, Completed, etc.) |
| `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 |
@ -356,7 +391,58 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu
---
## WorkflowTestFixture Helper Methods
When using [`WorkflowTestFixture`](../../src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs) for integration tests, use these helper methods to assert on activity behavior:
### Asserting Activity Outputs
Use `result.GetActivityOutput<T>(activity)` to retrieve the output value from an activity:
```csharp
var result = await _fixture.RunActivityAsync(activity);
var output = result.GetActivityOutput<object>(activity);
Assert.Equal(expectedValue, output);
```
### Asserting Activity Outcomes
Use `_fixture.GetOutcomes(result, activity)` to retrieve all outcomes produced by an activity:
```csharp
var result = await _fixture.RunActivityAsync(activity);
var outcomes = _fixture.GetOutcomes(result, activity);
Assert.Contains("ExpectedOutcome", outcomes);
```
Or use `_fixture.HasOutcome(result, activity, outcome)` to check for a specific outcome:
```csharp
var result = await _fixture.RunActivityAsync(activity);
Assert.True(_fixture.HasOutcome(result, activity, "Success"));
```
### Asserting Activity Status
Use `_fixture.GetActivityStatus(result, activity)` to check the execution status of an activity:
```csharp
var result = await _fixture.RunActivityAsync(activity);
var status = _fixture.GetActivityStatus(result, activity);
Assert.Equal(ActivityStatus.Faulted, status);
```
**Available activity statuses:**
- `ActivityStatus.Pending` - Activity is pending execution
- `ActivityStatus.Running` - Activity is currently running
- `ActivityStatus.Completed` - Activity completed successfully
- `ActivityStatus.Canceled` - Activity was canceled
- `ActivityStatus.Faulted` - Activity encountered an error
---
## Failure testing (faults & incidents)
- **Integration test faulted activities**: Use `_fixture.GetActivityStatus(result, activity)` to assert that a specific activity faulted, rather than checking workflow-level status.
- **Integration test faulted workflows**: build a workflow that throws and run via [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) — assert [`WorkflowInstance.Status`](../../src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs) == [`Faulted`](../../src/modules/Elsa.Workflows.Core/Enums/WorkflowStatus.cs) on the returned state or via [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs).
- **Component tests for recovery/resume**: persist a faulted instance (or cause a host restart scenario), run your recovery logic, and assert the final state.

View file

@ -152,4 +152,44 @@ public class WorkflowTestFixture
return await Services.RunWorkflowUntilEndAsync(definitionId, input);
}
/// <summary>
/// Gets the outcomes produced by a specific activity from the workflow result.
/// </summary>
/// <param name="result">The workflow run result</param>
/// <param name="activity">The activity to get outcomes for</param>
/// <returns>Collection of outcome names</returns>
public IEnumerable<string> GetOutcomes(RunWorkflowResult result, IActivity activity)
{
var activityContext = result.Journal.ActivityExecutionContexts
.FirstOrDefault(c => c.Activity.Id == activity.Id);
return activityContext?.GetOutcomes() ?? [];
}
/// <summary>
/// Checks if a specific activity produced a specific outcome.
/// </summary>
/// <param name="result">The workflow run result</param>
/// <param name="activity">The activity to check</param>
/// <param name="outcome">The outcome name to check for</param>
/// <returns>True if the activity produced the specified outcome</returns>
public bool HasOutcome(RunWorkflowResult result, IActivity activity, string outcome)
{
return GetOutcomes(result, activity).Contains(outcome);
}
/// <summary>
/// Gets the execution status of a specific activity from the workflow result.
/// </summary>
/// <param name="result">The workflow run result</param>
/// <param name="activity">The activity to get status for</param>
/// <returns>The activity status, or null if the activity wasn't found in the journal</returns>
public ActivityStatus? GetActivityStatus(RunWorkflowResult result, IActivity activity)
{
var activityContext = result.Journal.ActivityExecutionContexts
.FirstOrDefault(c => c.Activity.Id == activity.Id);
return activityContext?.Status;
}
}

View file

@ -7,6 +7,36 @@ public static class WorkflowExecutionResultExtensions
{
public static T GetActivityOutput<T>(this RunWorkflowResult result, IActivity activity, string? outputName = null)
{
return (T)result.WorkflowExecutionContext.GetOutputByActivityId(activity.Id, outputName)!;
var value = result.WorkflowExecutionContext.GetOutputByActivityId(activity.Id, outputName);
// If the value is already of the requested type, return it directly.
if (value is T tValue)
return tValue;
// Handle nulls for reference/nullable types.
if (value is null)
{
// If T is a reference type or nullable, default(T) is fine.
// Otherwise, this is a runtime error.
return default(T) is null ? default! : throw new InvalidCastException($"Cannot convert null to non-nullable type {typeof(T).FullName}.");
}
// Try to convert using System.Convert when possible (handles numeric casts like Double -> Int32).
try
{
var targetType = typeof(T);
// Unwrap nullable<T> to its underlying type for conversion.
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
var converted = Convert.ChangeType(value, underlyingType, System.Globalization.CultureInfo.InvariantCulture);
// If T is nullable and we converted to the underlying type, just cast.
return (T)converted!;
}
catch (Exception ex)
{
throw new InvalidCastException($"Unable to convert value of type '{value.GetType().FullName}' to type '{typeof(T).FullName}'.", ex);
}
}
}

View file

@ -1,5 +1,6 @@
using Elsa.Expressions.JavaScript.Activities;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Xunit.Abstractions;
@ -11,49 +12,56 @@ public class RunJavaScriptTests(ITestOutputHelper testOutputHelper)
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Theory(DisplayName = "RunJavaScript should execute valid scripts successfully")]
[InlineData("return 1 + 1;")]
[InlineData("return 'Hello World';")]
[InlineData("return 42;")]
public async Task Should_Execute_Valid_Scripts(string script)
[InlineData("return 1 + 1;", 2d)]
[InlineData("return 'Hello World';", "Hello World")]
[InlineData("return 42;", 42d)]
public async Task Should_Execute_Valid_Scripts(string script, object expectedOutput)
{
// Arrange
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
var result = await _fixture.RunActivityAsync(activity);
// Assert
Assert.Empty(_fixture.CapturingTextWriter.Lines);
// Assert - script returns expected value
var output = result.GetActivityOutput<object>(activity);
Assert.Equal(expectedOutput, output);
}
[Theory(DisplayName = "RunJavaScript should set outcomes correctly")]
[InlineData("setOutcome('Success');")]
[InlineData("setOutcomes(['Branch1', 'Branch2', 'Branch3']);")]
public async Task Should_Set_Outcomes(string script)
[InlineData("setOutcome('Success');", new[] { "Success" })]
[InlineData("setOutcomes(['Branch1', 'Branch2', 'Branch3']);", new[] { "Branch1", "Branch2", "Branch3" })]
public async Task Should_Set_Outcomes(string script, string[] expectedOutcomes)
{
// Arrange
var activity = new RunJavaScript { Script = new(script) };
// Act
await _fixture.RunActivityAsync(activity);
var result = await _fixture.RunActivityAsync(activity);
// Assert
Assert.Null(_fixture.CapturingTextWriter.Lines.FirstOrDefault());
// Assert - activity produced expected outcomes
var outcomes = _fixture.GetOutcomes(result, activity).ToArray();
Assert.Equal(expectedOutcomes.Length, outcomes.Length);
foreach (var expectedOutcome in expectedOutcomes)
{
Assert.Contains(expectedOutcome, outcomes);
}
}
[Theory(DisplayName = "RunJavaScript should not execute invalid scripts")]
[Theory(DisplayName = "RunJavaScript should produce null output for empty or whitespace scripts")]
[InlineData("")]
[InlineData(" ")]
public async Task Should_Not_Execute_Invalid_Scripts(string script)
public async Task Should_Produce_Null_Output_For_Empty_Scripts(string script)
{
// Arrange
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
var result = await _fixture.RunActivityAsync(activity);
// Assert
Assert.Empty(_fixture.CapturingTextWriter.Lines);
// Assert - empty/whitespace scripts produce no output
var output = result.GetActivityOutput<object>(activity);
Assert.Null(output);
}
[Fact(DisplayName = "RunJavaScript should access workflow variables")]
@ -62,17 +70,19 @@ public class RunJavaScriptTests(ITestOutputHelper testOutputHelper)
// Arrange
var myVar = new Variable<int>("MyVar", 100);
var script = "return getMyVar();";
var runJavaScript = new RunJavaScript { Script = new(script), Result = new() };
var workflow = new Workflow
{
Root = new RunJavaScript { Script = new(script), Result = new() },
Root = runJavaScript,
Variables = { myVar }
};
// Act
await _fixture.RunActivityAsync(workflow);
var result = await _fixture.RunActivityAsync(workflow);
// Assert - workflow completes successfully
Assert.Empty(_fixture.CapturingTextWriter.Lines);
// Assert - variable was accessed and returned
var output = result.GetActivityOutput<int>(runJavaScript);
Assert.Equal(100, output);
}
[Fact(DisplayName = "RunJavaScript should execute complex script with multiple statements and outcomes")]
@ -93,9 +103,27 @@ public class RunJavaScriptTests(ITestOutputHelper testOutputHelper)
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
var result = await _fixture.RunActivityAsync(activity);
// Assert - workflow completes successfully
Assert.Empty(_fixture.CapturingTextWriter.Lines);
// Assert - script returns calculated sum
var output = result.GetActivityOutput<int>(activity);
Assert.Equal(30, output);
}
[Theory(DisplayName = "RunJavaScript should fault on invalid JavaScript syntax")]
[InlineData("this is not valid javascript")]
[InlineData("return unclosedBracket(;")]
[InlineData("undefined.property.access")]
public async Task Should_Fault_On_Invalid_JavaScript(string script)
{
// Arrange
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
var result = await _fixture.RunActivityAsync(activity);
// Assert - activity should be in faulted state
var activityStatus = _fixture.GetActivityStatus(result, activity);
Assert.Equal(ActivityStatus.Faulted, activityStatus);
}
}