Add integration tests for RunJavaScript activity and introduce WorkflowTestFixture

- Implement tests to validate execution of valid and invalid scripts, outcome settings, workflow variable access, and complex script handling.
- Add `WorkflowTestFixture` to streamline integration testing setup and execution.
This commit is contained in:
Sipke Schoorstra 2025-10-21 20:02:27 +02:00
parent 5c655e0a44
commit efe849d0bc
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
7 changed files with 268 additions and 14 deletions

View file

@ -0,0 +1,155 @@
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Models;
using Elsa.Workflows.State;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Testing.Shared;
/// <summary>
/// A test fixture for integration testing workflows and activities.
/// Provides a fluent API to configure services, run workflows, and capture output.
/// </summary>
public class WorkflowTestFixture
{
private readonly TestApplicationBuilder _testApplicationBuilder;
private IServiceProvider? _services;
/// <summary>
/// Initializes a new instance of the <see cref="WorkflowTestFixture"/> class.
/// </summary>
/// <param name="testOutputHelper">The test output helper</param>
public WorkflowTestFixture(ITestOutputHelper testOutputHelper)
{
_testApplicationBuilder = new TestApplicationBuilder(testOutputHelper);
CapturingTextWriter = new CapturingTextWriter();
_testApplicationBuilder.WithCapturingTextWriter(CapturingTextWriter);
}
/// <summary>
/// Gets the capturing text writer that captures standard output from WriteLine activities.
/// </summary>
public CapturingTextWriter CapturingTextWriter { get; }
/// <summary>
/// Gets the service provider. Throws if Build() hasn't been called yet.
/// </summary>
private IServiceProvider Services => _services ?? throw new InvalidOperationException("Build() must be called before accessing services");
/// <summary>
/// Configures Elsa features.
/// </summary>
/// <param name="configure">Action to configure Elsa</param>
/// <returns>The fixture instance for method chaining</returns>
[UsedImplicitly]
public WorkflowTestFixture ConfigureElsa(Action<IModule> configure)
{
_testApplicationBuilder.ConfigureElsa(configure);
return this;
}
/// <summary>
/// Configures the service collection.
/// </summary>
/// <param name="configure">Action to configure the service collection</param>
/// <returns>The fixture instance for method chaining</returns>
[UsedImplicitly]
public WorkflowTestFixture ConfigureServices(Action<IServiceCollection> configure)
{
_testApplicationBuilder.ConfigureServices(configure);
return this;
}
/// <summary>
/// Adds a workflow to the service provider.
/// </summary>
[UsedImplicitly]
public WorkflowTestFixture AddWorkflow<T>() where T : IWorkflow
{
_testApplicationBuilder.AddWorkflow<T>();
return this;
}
/// <summary>
/// Adds activities from the assembly containing the specified type.
/// </summary>
[UsedImplicitly]
public WorkflowTestFixture AddActivitiesFrom<T>()
{
_testApplicationBuilder.AddActivitiesFrom<T>();
return this;
}
/// <summary>
/// Adds workflows from the specified relative directory.
/// </summary>
/// <param name="directory">The path segments of the directory</param>
[UsedImplicitly]
public WorkflowTestFixture WithWorkflowsFromDirectory(params string[] directory)
{
_testApplicationBuilder.WithWorkflowsFromDirectory(directory);
return this;
}
/// <summary>
/// Builds the service provider and populates registries.
/// Must be called before running workflows.
/// </summary>
public async Task<WorkflowTestFixture> BuildAsync()
{
_services = _testApplicationBuilder.Build();
await Services.PopulateRegistriesAsync();
return this;
}
/// <summary>
/// Runs a workflow and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="workflow">The workflow to run</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync(workflow, cancellationToken: cancellationToken);
}
/// <summary>
/// Runs an activity wrapped in a workflow and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="activity">The activity to run</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync(activity, cancellationToken: cancellationToken);
}
/// <summary>
/// Runs a workflow by definition ID and returns the workflow state.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="definitionId">The workflow definition ID</param>
/// <param name="input">Optional input dictionary</param>
/// <returns>The workflow state after execution</returns>
public async Task<WorkflowState> RunWorkflowAsync(string definitionId, IDictionary<string, object>? input = null)
{
if (_services == null)
await BuildAsync();
return await Services.RunWorkflowUntilEndAsync(definitionId, input);
}
}

View file

@ -25,7 +25,7 @@ public class RunJavaScript : CodeActivity<object?>
/// <inheritdoc />
public RunJavaScript(string script, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
{
Script = new Input<string>(script);
Script = new(script);
}
/// <summary>

View file

@ -2,14 +2,13 @@
using System.Numerics;
using System.Text.Json;
using Elsa.Common.Converters;
using Elsa.Expressions.Models;
using Elsa.Expressions.JavaScript.Contracts;
using Elsa.Expressions.Models;
using Elsa.Testing.Shared;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.Expressions.JavaScript.IntegrationTests;
namespace Elsa.JavaScript.IntegrationTests;
public class JsonConverterTest(ITestOutputHelper testOutputHelper)
{
private readonly IServiceProvider _serviceProvider = new TestApplicationBuilder(testOutputHelper).Build();
@ -23,7 +22,7 @@ public class JsonConverterTest(ITestOutputHelper testOutputHelper)
}";
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var result = (await javaScriptEvaluator.EvaluateAsync(script, typeof(ExpandoObject), expressionExecutionContext))!;
var options = new JsonSerializerOptions()
var options = new JsonSerializerOptions
{
Converters = { new BigIntegerJsonConverter() }
};
@ -47,9 +46,9 @@ public class JsonConverterTest(ITestOutputHelper testOutputHelper)
}
[Fact(DisplayName = "BigIntegerJsonConverter Deserialize")]
public async Task Test3()
public Task Test3()
{
var options = new JsonSerializerOptions()
var options = new JsonSerializerOptions
{
Converters = { new BigIntegerJsonConverter() }
};
@ -57,5 +56,6 @@ public class JsonConverterTest(ITestOutputHelper testOutputHelper)
BigInteger bigInteger = JsonSerializer.Deserialize<BigInteger>("7239948466988781569", options);
Assert.Equal(7239948466988781569, bigInteger);
return Task.CompletedTask;
}
}

View file

@ -1,13 +1,12 @@
using System.Text.Json;
using Elsa.Expressions.Models;
using Elsa.Expressions.JavaScript.Contracts;
using Elsa.Expressions.Models;
using Elsa.Testing.Shared;
using Elsa.Workflows.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.Expressions.JavaScript.IntegrationTests;
namespace Elsa.JavaScript.IntegrationTests;
public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
{

View file

@ -0,0 +1,101 @@
using Elsa.Expressions.JavaScript.Activities;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Xunit.Abstractions;
namespace Elsa.JavaScript.IntegrationTests;
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)
{
// Arrange
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
// Assert
Assert.Empty(_fixture.CapturingTextWriter.Lines);
}
[Theory(DisplayName = "RunJavaScript should set outcomes correctly")]
[InlineData("setOutcome('Success');")]
[InlineData("setOutcomes(['Branch1', 'Branch2', 'Branch3']);")]
public async Task Should_Set_Outcomes(string script)
{
// Arrange
var activity = new RunJavaScript { Script = new(script) };
// Act
await _fixture.RunActivityAsync(activity);
// Assert
Assert.Null(_fixture.CapturingTextWriter.Lines.FirstOrDefault());
}
[Theory(DisplayName = "RunJavaScript should not execute invalid scripts")]
[InlineData("")]
[InlineData(" ")]
public async Task Should_Not_Execute_Invalid_Scripts(string script)
{
// Arrange
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
// Assert
Assert.Empty(_fixture.CapturingTextWriter.Lines);
}
[Fact(DisplayName = "RunJavaScript should access workflow variables")]
public async Task Should_Access_Workflow_Variables()
{
// Arrange
var myVar = new Variable<int>("MyVar", 100);
var script = "return getMyVar();";
var workflow = new Workflow
{
Root = new RunJavaScript { Script = new(script), Result = new() },
Variables = { myVar }
};
// Act
await _fixture.RunActivityAsync(workflow);
// Assert - workflow completes successfully
Assert.Empty(_fixture.CapturingTextWriter.Lines);
}
[Fact(DisplayName = "RunJavaScript should execute complex script with multiple statements and outcomes")]
public async Task Should_Execute_Complex_Script_With_Multiple_Statements()
{
// Arrange
var script = @"
var x = 10;
var y = 20;
var sum = x + y;
if (sum > 25) {
setOutcome('Large');
} else {
setOutcome('Small');
}
return sum;
";
var activity = new RunJavaScript { Script = new(script), Result = new() };
// Act
await _fixture.RunActivityAsync(activity);
// Assert - workflow completes successfully
Assert.Empty(_fixture.CapturingTextWriter.Lines);
}
}

View file

@ -1,13 +1,12 @@
using System.Text.Json;
using Elsa.Expressions.Models;
using Elsa.Expressions.JavaScript.Contracts;
using Elsa.Expressions.Models;
using Elsa.Testing.Shared;
using Elsa.Workflows.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.Expressions.JavaScript.IntegrationTests;
namespace Elsa.JavaScript.IntegrationTests;
public class ToJsonTests(ITestOutputHelper testOutputHelper)
{

View file

@ -11,7 +11,7 @@ public class ForkTests
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
private IServiceProvider _services;
private readonly IServiceProvider _services;
public ForkTests(ITestOutputHelper testOutputHelper)
{