Merge branch 'feature/unit-test-coverage-writeline' into feat/unit-test-coverage-sendhttprequest

This commit is contained in:
lucas.hipolito 2025-10-13 19:22:22 +02:00
commit a3900c913f
4 changed files with 284 additions and 97 deletions

View file

@ -306,6 +306,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.Integratio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-4667-ADC2-37C62C9C2574}"
ProjectSection(SolutionItems) = preProject
doc\qa\test-guidelines.md = doc\qa\test-guidelines.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -708,6 +713,7 @@ Global
{51C39AF0-4F41-4FC1-AEBD-D1494407D3F9} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
{DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92}
{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA} = {18453B51-25EB-4317-A4B3-B10518252E92}
{0478E6EA-DCB2-4667-ADC2-37C62C9C2574} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}

269
doc/qa/test-guidelines.md Normal file
View file

@ -0,0 +1,269 @@
# Elsa Core — Testing Strategy
## Purpose
This document is a practical test guideline. It tells you *what* to test, *when* to test it, and *how* to write deterministic, actionable tests using the repository's existing test helpers and patterns.
---
## Summary
The philosophy of testing in Elsa can be summarized as:
***Whenever a test fails, it should provide a clear direction towards the cause of the problem.***
Tests should be fast, deterministic, and precise: they should pinpoint the failing subsystem (activity, invoker, persistence, scheduler, etc.) with minimal noise.
For contributors, tests are the first line of code review: they must document intended behaviour and prevent regressions.
---
## Glossary
- **Activity** — a single unit of workflow logic (e.g., WriteLine, If, ForEach, HttpRequest).
- **Workflow** — a graph of activities connected by control flow.
- **Workflow Definition** — a serializable representation of a workflow (JSON or code).
- **Workflow Instance** — a persisted execution of a workflow definition, including state, variables, and journal.
- **Bookmark** — a pause point in a workflow where execution is suspended until an external event resumes it.
- **Invoker** — the core engine component that orchestrates workflow execution, including activity execution, scheduling, and state transitions.
- **Scheduler** — the subsystem that manages background tasks, timers, and resumption of workflows.
- **Journal** — a log of all activity executions and state changes in a workflow instance.
- **Persistence** — the storage mechanism for workflow definitions and instances (e.g., EF Core, MongoDB).
- **Unit Test** — a test that verifies a small, isolated piece of code (such as a function or method) works as expected.
- **Integration Test** — a test that verifies the interaction between multiple components or subsystems works as expected.
- **Component Test** — a test that verifies the behavior of a larger part of the system, often involving persistence and external dependencies.
---
## High-level testing pyramid
- **Unit tests** — single-class logic (activities, converters, expression evaluators, serializers, service providers). Fast; no persistence.
- **Integration tests** — multiple Elsa subsystems together (e.g., invoker + activities + registries). In-process; may deserialize workflow JSON. Use [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) when using existing definitions.
- **Component tests** — persisted behaviour, journal/instance store assertions, bookmarks/resumption across lifecycle boundaries. Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) to instantiate and [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) queries for assertions.
Each test layer has distinct goals and clear boundaries — see [**Which parts of Elsa to test**](#which-parts-of-elsa-to-test-and-which-test-types-to-use) for precise mapping of which aspects belong to which layer.
---
## Quick Start for Contributors
**Before you writ e a test:**
1. ✅ Understand what you're testing (see [**Which parts of Elsa to test**](#which-parts-of-elsa-to-test-and-which-test-types-to-use))
2. ✅ Choose the right test layer (unit vs integration vs component)
3. ✅ Use existing helpers (don't reinvent - see [**Test Helpers Reference**](#test-helpers-reference-quick-lookup))
**5-Minute Checklist:**
- [ ] 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)
- [ ] 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`)
---
## 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). 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).
---
## Which parts of Elsa to test, and which test types to use
This section maps Elsa aspects to the exact kinds of tests you should write, with examples and code patterns referencing repository conventions.
### Activities
#### **Unit tests:**
- Test the activity class logic only (no persistence, no scheduler). Cover configuration permutations and boundary inputs.
- 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
[Fact]
public async Task Should_Set_Variable_Integer()
{
// Arrange
const int expected = 42;
var variable = new Variable<int>("myVar", 0, "myVar");
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
// Act
var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable);
// Assert
var result = context.Get(variable);
Assert.Equal(expected, result);
}
```
#### **Integration tests:**
- Place the activity inside a minimal workflow definition and run via [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs). Assert outputs/variables and that the activity integrates correctly with preceding/following activities.
- If activity creates bookmarks or relies on scheduler semantics, integration tests should resume bookmarks via the engine APIs to validate resumption.
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.
### Workflow execution (invoker, middleware, bookmarks)
#### Unit tests:
- Rare: low-level pure helpers in the invoker may have unit tests for edge cases. Most invoker behavior requires integration testing.
#### Integration tests:
- Use [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) with small workflows to test variables propagation, branch logic (If/ForEach/Parallel), expression evaluation, and `RunAsynchronously` flags.
- When a workflow schedules async child work (bookmarks), simulate resumption by calling resume APIs.
Call the workflow runner to execute a workflow object or a loaded definition. Prefer this when asserting logical flow and outputs.
```csharp
var runner = serviceProvider.GetRequiredService<IWorkflowRunner>();
await serviceProvider.PopulateRegistriesAsync();
var runResult = await runner.RunAsync(workflow);
Assert.Equal(WorkflowStatus.Finished, runResult.WorkflowInstance!.Status);
```
#### Component tests (persistence & resumption):
- Start a workflow that creates a bookmark. Persisted instance must be found via [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) after the creation point. Simulate host restart by disposing and rebuilding the service provider (keeping the same persistence store) and resume the bookmark to assert resumption completes.
**Code pattern to resume a bookmark (integration/component):**
```csharp
// assume instanceId found via RunAsync or correlation id
await workflowTriggerService.ResumeAsync(instanceId, activityId, input, CancellationToken.None);
var resumed = await runner.RunAsync(workflowInstance);
Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status);
```
---
## Test Helpers Reference (Quick Lookup)
| Helper | Purpose | Use When |
|-------------------------------------------|--------------------------------------------|-------------------------------------------------------------------------|
| `TestApplicationBuilder` | Build test service provider | All tests as entry point, <br/>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. <br/>Integration tests only |
| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) |
| `RunWorkflowUntilEndAsync` | Drive workflow to completion. | Complex resumption scenarios. <br/>Integration tests only |
---
## Decision helper (what to add — follow in order)
1. **Changed code is a single activity class with no persistence/external calls?** → Unit test only.
2. **Change touches multiple activities or workflow logic (If, ForEach, Parallel, Flow activities, etc.)?** → Integration test using [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and a small workflow.
3. **Change touches invoker/scheduler/bookmarks or similar multi-component feature?** → Integration test using [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and a small workflow. If persistence semantics change, add component tests.
4. **Change touches persistence/serializers or requires durable evidence (journal, bookmarks)?** → Component tests against [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs).
**Rule of thumb:**
- If its about **internal logic**, write a **unit** test.
- If its about **collaboration between components**, write an **integration** test.
- If its about **end-to-end feature behavior**, write a **component** test
When in doubt, add the minimal unit tests plus one integration test that reproduces the scenario.
---
## 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.
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.
---
## Failure testing (faults & incidents)
- **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.
---
## Practical test recipes & snippets (copy/paste-ready)
### Unit test (activity) — pattern
```csharp
[Fact]
public async Task MyActivity_Test()
{
var activity = new ActivityToTest();
// Act
var context = await ActivityTestHelper.ExecuteActivityAsync(activity);
// assert behavior of activity in isolation
}
```
### Integration test — pattern using [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs)
```csharp
[Fact]
public async Task Workflow_With_MyActivity_Completes()
{
var sp = new TestApplicationBuilder(testOutput).Build();
await sp.PopulateRegistriesAsync();
var runner = sp.GetRequiredService<IWorkflowRunner>();
var workflow = new MyWorkflowDefinition();
var result = await runner.RunAsync(workflow);
Assert.Equal(WorkflowStatus.Finished, result.WorkflowInstance!.Status);
}
```
### Component test
```csharp
[Fact]
public async Task Workflow_Persists_Instance_And_Journal()
{
var sp = new TestApplicationBuilder(testOutput)
.Build();
var runner = sp.GetRequiredService<AsyncWorkflowRunner>();
var result = await runner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(someDefinitionId, VersionOptions.Published));
result.WorkflowExecutionContext.Status.Should().Be(WorkflowStatus.Finished);
}
```
---
## FAQ (quick pointers)
**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 `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) 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.
---
## Appendix — examples in the repository (where to look)
Search the `test/` tree for examples that follow the above patterns:
- 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).

View file

@ -1,11 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ElsaServer": {
"Url": "https://localhost:5001/elsa/api"
},

View file

@ -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<string>(expectedText);
var mockTextWriter = Substitute.For<TextWriter>();
var mockProvider = Substitute.For<IStandardOutStreamProvider>();
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<string> textFunc = () => expectedText;
var mockTextWriter = Substitute.For<TextWriter>();
var mockProvider = Substitute.For<IStandardOutStreamProvider>();
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<ExpressionExecutionContext, string?> textFunc = _ => expectedText;
var mockTextWriter = Substitute.For<TextWriter>();
var mockProvider = Substitute.For<IStandardOutStreamProvider>();
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<string>(expectedText);
var mockTextWriter = Substitute.For<TextWriter>();
var mockProvider = Substitute.For<IStandardOutStreamProvider>();
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 () =>