From fe80505f21ed5e3ce4fc6da85b7b08738e13033e Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Fri, 10 Oct 2025 10:30:24 +0200 Subject: [PATCH 1/7] Unit testing coverage for WriteLine activity --- .../Console/WriteLineTests.cs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs diff --git a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs new file mode 100644 index 000000000..401906a19 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs @@ -0,0 +1,174 @@ +using Elsa.Activities.UnitTests.Helpers; +using Elsa.Expressions.Models; +using Elsa.Workflows; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Elsa.Activities.UnitTests.Console; + +public class WriteLineTests +{ + [Fact] + public async Task Should_Write_String_Literal_To_Output() + { + // Arrange + const string expectedText = "Hello, World!"; + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(expectedText); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Write_Literal_Expression_To_Output() + { + // Arrange + const string expectedText = "Literal expression"; + var literal = new Literal(expectedText); + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(literal); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Write_Delegate_Function_Result_To_Output() + { + // Arrange + const string expectedText = "Function result"; + Func textFunc = () => expectedText; + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(textFunc); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Write_Expression_Context_Function_Result_To_Output() + { + // Arrange + const string expectedText = "Context function result"; + Func textFunc = _ => expectedText; + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(textFunc); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Write_Input_Value_To_Output() + { + // Arrange + const string expectedText = "Input value"; + var input = new Input(expectedText); + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(input); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Write_Null_Value_To_Output() + { + // Arrange + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(new Input(default(string)!)); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(Arg.Is(s => s == null)); + } + + [Fact] + public async Task Should_Write_Empty_String_To_Output() + { + // Arrange + const string expectedText = ""; + var mockTextWriter = Substitute.For(); + var mockProvider = Substitute.For(); + mockProvider.GetTextWriter().Returns(mockTextWriter); + + var writeLine = new WriteLine(expectedText); + + // Act + await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => + { + services.AddSingleton(mockProvider); + }); + + // Assert + mockTextWriter.Received(1).WriteLine(expectedText); + } + + [Fact] + public async Task Should_Use_Default_Provider_When_None_Configured() + { + // Arrange + const string expectedText = "Default provider test"; + var writeLine = new WriteLine(expectedText); + + // Act & Assert - Should not throw exception when no provider is configured + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(writeLine)); + + Assert.Null(exception); + } +} From d0aafe6d4ebb22562874424f6109d3441e85b473 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 15:02:38 +0200 Subject: [PATCH 2/7] Removing out of scope tests --- .../Console/WriteLineTests.cs | 92 +------------------ 1 file changed, 2 insertions(+), 90 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs index 401906a19..cebead58a 100644 --- a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs @@ -29,94 +29,6 @@ public class WriteLineTests mockTextWriter.Received(1).WriteLine(expectedText); } - [Fact] - public async Task Should_Write_Literal_Expression_To_Output() - { - // Arrange - const string expectedText = "Literal expression"; - var literal = new Literal(expectedText); - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - - var writeLine = new WriteLine(literal); - - // Act - await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => - { - services.AddSingleton(mockProvider); - }); - - // Assert - mockTextWriter.Received(1).WriteLine(expectedText); - } - - [Fact] - public async Task Should_Write_Delegate_Function_Result_To_Output() - { - // Arrange - const string expectedText = "Function result"; - Func textFunc = () => expectedText; - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - - var writeLine = new WriteLine(textFunc); - - // Act - await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => - { - services.AddSingleton(mockProvider); - }); - - // Assert - mockTextWriter.Received(1).WriteLine(expectedText); - } - - [Fact] - public async Task Should_Write_Expression_Context_Function_Result_To_Output() - { - // Arrange - const string expectedText = "Context function result"; - Func textFunc = _ => expectedText; - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - - var writeLine = new WriteLine(textFunc); - - // Act - await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => - { - services.AddSingleton(mockProvider); - }); - - // Assert - mockTextWriter.Received(1).WriteLine(expectedText); - } - - [Fact] - public async Task Should_Write_Input_Value_To_Output() - { - // Arrange - const string expectedText = "Input value"; - var input = new Input(expectedText); - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - - var writeLine = new WriteLine(input); - - // Act - await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => - { - services.AddSingleton(mockProvider); - }); - - // Assert - mockTextWriter.Received(1).WriteLine(expectedText); - } - [Fact] public async Task Should_Write_Null_Value_To_Output() { @@ -162,8 +74,8 @@ public class WriteLineTests public async Task Should_Use_Default_Provider_When_None_Configured() { // Arrange - const string expectedText = "Default provider test"; - var writeLine = new WriteLine(expectedText); + const string textToWrite = "Default provider test"; + var writeLine = new WriteLine(textToWrite); // Act & Assert - Should not throw exception when no provider is configured var exception = await Record.ExceptionAsync(async () => From ab7d0f6d55f7c643563c91654c8d55fddd26dfbd Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 17:07:56 +0200 Subject: [PATCH 3/7] Updated documentation to reflect test suite helpers --- doc/qa/test-guidelines.md | 78 +++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 0bc9fa3a5..7064bfbdc 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -54,7 +54,6 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o - [ ] Read the relevant section below for your change type: - Changed activity logic or created a new activity? → See [Activities](#activities) - Changed workflow execution? → See [Workflows execution](#workflow-execution-invoker-middleware-bookmarks) - - Changed persistence? → See [Persistence & serialization](#persistence--serialization) - [ ] Follow steps and code patterns in that section - [ ] Run tests locally: `dotnet test` - [ ] Verify no flaky behavior (run 10 times: `dotnet test --no-build -- repeat 10`) @@ -63,7 +62,7 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o ## Characteristics for testing -- **Activities:** First-class pluggable units. Each activity implements execution logic and interacts with the [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs). Many activity tests in the repository use [`RunActivityAsync`](../../src/common/Elsa.Testing.Shared.Integration/RunActivityExtensions.cs) to create the required context and invoke the activity inline. More details in [**Activities**](#activities). +- **Activities:** First-class pluggable units. Each activity implements execution logic and interacts with the [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs). More details in [**Activities**](#activities). - **Workflows:** Graphs of activities. A workflow can run synchronously or schedule asynchronous work (bookmarks, timers). When you run a workflow in-process with [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs), the runner will return when synchronous work completes. Some activities set `RunAsynchronously` causing background scheduling — tests need to take care when asserting. More details in [**Workflow execution**](#workflow-execution-invoker-middleware-bookmarks). @@ -77,21 +76,25 @@ This section maps Elsa aspects to the exact kinds of tests you should write, wit #### **Unit tests:** - Test the activity class logic only (no persistence, no scheduler). Cover configuration permutations and boundary inputs. -- Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) + [`RunActivityAsync`](../../src/common/Elsa.Testing.Shared.Integration/RunActivityExtensions.cs) to obtain an [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs) and run the activity. +- Use [`ActivityTestHelper`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs), `ExecuteActivityAsync` method to run the activity and obtain an [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs) for assertions. **Example:** ```csharp -// Arrange -var serviceProvider = new TestApplicationBuilder(testOutputHelper) - .WithCapturingTextWriter(capturingTextWriter) - .Build(); +[Fact] +public async Task Should_Set_Variable_Integer() +{ + // Arrange + const int expected = 42; + var variable = new Variable("myVar", 0, "myVar"); + var setVariable = new SetVariable(variable, new Input(expected)); -// Act -var writeLine = new WriteLine("Hello world!"); -await serviceProvider.RunActivityAsync(writeLine); + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable); -// Assert -Assert.Equal("Hello world!", capturingTextWriter.Lines.Single()); + // Assert + var result = context.Get(variable); + Assert.Equal(expected, result); +} ``` #### **Integration tests:** @@ -137,14 +140,14 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status); ## Test Helpers Reference (Quick Lookup) -| Helper | Purpose | Use When | -|--------|------------------------------------------------------------------------------|-----------------------------------------------------------| -| `TestApplicationBuilder` | Build test service provider | All tests (entry point) | -| `RunActivityAsync` | Run single activity | Unit testing activities | -| `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests | -| `PopulateRegistriesAsync` | Register types for JSON deserialization. | Loading JSON workflows.
Integration tests only | -| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) | -| `RunWorkflowUntilEndAsync` | Drive workflow to completion. | Complex resumption scenarios.
Integration tests only | +| Helper | Purpose | Use When | +|-------------------------------------------|--------------------------------------------|-------------------------------------------------------------------------| +| `TestApplicationBuilder` | Build test service provider | All tests as entry point,
except activities unit tests (see below) | +| `ActivityTestHelper.ExecuteActivityAsync` | Run single activity, with isolated context | Unit testing activities | +| `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests | +| `PopulateRegistriesAsync` | Register types for JSON deserialization. | Loading JSON workflows.
Integration tests only | +| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) | +| `RunWorkflowUntilEndAsync` | Drive workflow to completion. | Complex resumption scenarios.
Integration tests only | --- @@ -168,10 +171,9 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu ## Deterministic patterns to avoid flaky tests -1. **Prefer returned state from [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs).** Always inspect [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) results first — it is deterministic for synchronous workflows. +1. **For activities unit tests, prefer returned state from [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows. 2. **Resume bookmarks explicitly.** Do not wait for external schedulers — call the engine's resume/trigger APIs in your test to continue execution. -3. **Locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries. -4. **Use short polling where necessary.** If you must poll the instance store (e.g., testing asynchronous controllers), use a short interval and a deterministic timeout (helper code snippets in examples above). +3. **For integration tests, Locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries. --- @@ -187,12 +189,12 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu ```csharp [Fact] -public async Task MyActivity_WritesExpectedOutput() +public async Task MyActivity_Test() { - var sp = new TestApplicationBuilder(testOutput).Build(); - var activity = new MyActivity { Input = "x" }; + var activity = new ActivityToTest(); - await sp.RunActivityAsync(activity); + // Act + var context = await ActivityTestHelper.ExecuteActivityAsync(activity); // assert behavior of activity in isolation } @@ -216,7 +218,7 @@ public async Task Workflow_With_MyActivity_Completes() } ``` -### Component test — pattern asserting persisted state +### Component test ```csharp [Fact] @@ -234,17 +236,23 @@ var result = await runner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionH ## FAQ (quick pointers) -**Q: How do I import workflow definitions in tests and where do I put the workflow definitions?** -A: For JSON-defined workflows use the repo's test integration helpers ([`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) or the test registration helpers in `test/common`). See integration test examples in the test tree. Leave the definitions next to the tests that use them. +**Q: How do I import workflow definitions in tests and where do I put them?** + +A: For JSON-defined workflows use the repo's test integration helpers ([`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) or the test registration helpers in `test/common`). +See integration test examples in the test tree. +Leave the definitions next to the tests that use them. **Q: Which helper should I use to run a workflow?** -A: Prefer [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for in-process deterministic runs. For activities use [`RunActivityAsync`](../../src/common/Elsa.Testing.Shared.Integration/RunActivityExtensions.cs) via [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs). + +A: Prefer [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for in-process deterministic runs. For activities use `ExecuteActivityAsync` via [`ActivityTestHelper`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs). **Q: How do I check persisted journal entries?** + A: Query [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) and inspect the persisted journal on the instance. Use deterministic instance id or correlation id to locate the exact instance. **Q: Do I need a new helper to wait for workflow completion?** -A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and integration helpers that cover all necessary scenarios. + +A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for integration tests and [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs) for activity unit tests, as well as integration helpers that cover all necessary scenarios. --- @@ -252,9 +260,9 @@ A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Cont Search the `test/` tree for examples that follow the above patterns: -- Unit activity examples: `test/unit/*` (look for [`RunActivityAsync`](../../src/common/Elsa.Testing.Shared.Integration/RunActivityExtensions.cs) usage). -- Integration workflow examples: `test/integration/*` (look for [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) and [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) usage). -- Component scenarios exercising persistence: `test/component/*` (look for [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) scaffolds and [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) assertions). +- Unit test activity examples: `test/unit/Elsa.Activities.UnitTests` (look for [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs) usage). +- Integration workflow examples: `test/integration/Elsa.*.IntegrationTests` (look for [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) and [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) usage). +- Component scenarios exercising persistence: `test/component/Elsa.Workflows.ComponentTests (look for [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) scaffolds and [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) assertions). From 4b0a27cc7f484c7784e6947586498aa9941c8449 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 13 Oct 2025 19:23:42 +0200 Subject: [PATCH 4/7] Update test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs index cebead58a..2467f21fe 100644 --- a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs @@ -37,7 +37,7 @@ public class WriteLineTests var mockProvider = Substitute.For(); mockProvider.GetTextWriter().Returns(mockTextWriter); - var writeLine = new WriteLine(new Input(default(string)!)); + var writeLine = new WriteLine(new Input((string?)null)); // Act await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => From 7147eb5124535407aa8cf079fa59528be3015e4f Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 19:31:03 +0200 Subject: [PATCH 5/7] Improving maintainability of tests --- .../Console/WriteLineTests.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs index cebead58a..f17dfdfd4 100644 --- a/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs @@ -1,5 +1,4 @@ using Elsa.Activities.UnitTests.Helpers; -using Elsa.Expressions.Models; using Elsa.Workflows; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -8,45 +7,47 @@ namespace Elsa.Activities.UnitTests.Console; public class WriteLineTests { + private readonly TextWriter _mockTextWriter; + private readonly IStandardOutStreamProvider _mockProvider; + + public WriteLineTests() + { + _mockTextWriter = Substitute.For(); + _mockProvider = Substitute.For(); + _mockProvider.GetTextWriter().Returns(_mockTextWriter); + } + [Fact] public async Task Should_Write_String_Literal_To_Output() { // Arrange const string expectedText = "Hello, World!"; - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - var writeLine = new WriteLine(expectedText); // Act await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => { - services.AddSingleton(mockProvider); + services.AddSingleton(_mockProvider); }); // Assert - mockTextWriter.Received(1).WriteLine(expectedText); + _mockTextWriter.Received(1).WriteLine(expectedText); } [Fact] public async Task Should_Write_Null_Value_To_Output() { // Arrange - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - var writeLine = new WriteLine(new Input(default(string)!)); // Act await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => { - services.AddSingleton(mockProvider); + services.AddSingleton(_mockProvider); }); // Assert - mockTextWriter.Received(1).WriteLine(Arg.Is(s => s == null)); + _mockTextWriter.Received(1).WriteLine(Arg.Is(s => s == null)); } [Fact] @@ -54,20 +55,16 @@ public class WriteLineTests { // Arrange const string expectedText = ""; - var mockTextWriter = Substitute.For(); - var mockProvider = Substitute.For(); - mockProvider.GetTextWriter().Returns(mockTextWriter); - var writeLine = new WriteLine(expectedText); // Act await ActivityTestHelper.ExecuteActivityAsync(writeLine, services => { - services.AddSingleton(mockProvider); + services.AddSingleton(_mockProvider); }); // Assert - mockTextWriter.Received(1).WriteLine(expectedText); + _mockTextWriter.Received(1).WriteLine(expectedText); } [Fact] From 3d15baf0f2ef13f37226fa2432f80d01c8e825e7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Oct 2025 12:45:18 +0200 Subject: [PATCH 6/7] Update doc/qa/test-guidelines.md --- doc/qa/test-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 7064bfbdc..4bab1304e 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -171,7 +171,7 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu ## Deterministic patterns to avoid flaky tests -1. **For activities unit tests, prefer returned state from [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows. +1. **For activity unit tests, prefer returned state from [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows. 2. **Resume bookmarks explicitly.** Do not wait for external schedulers — call the engine's resume/trigger APIs in your test to continue execution. 3. **For integration tests, Locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries. From 35704e91662c57fe873d886362fbf6b8c18e3846 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Oct 2025 12:45:23 +0200 Subject: [PATCH 7/7] Update doc/qa/test-guidelines.md --- doc/qa/test-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 4bab1304e..6fbafb73d 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -173,7 +173,7 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu 1. **For activity unit tests, prefer returned state from [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows. 2. **Resume bookmarks explicitly.** Do not wait for external schedulers — call the engine's resume/trigger APIs in your test to continue execution. -3. **For integration tests, Locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries. +3. **For integration tests, locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries. ---