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:
parent
07ba8184b8
commit
ad0dfad77e
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue