From 32f6a393a25845fe4a43462ab824acc3e7b0764d Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 25 Sep 2025 12:58:31 +0200 Subject: [PATCH 01/40] WIP: initial docs dump --- Elsa.sln | 6 ++++++ doc/qa/test-guidelines.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 doc/qa/test-guidelines.md diff --git a/Elsa.sln b/Elsa.sln index e3daac0f8..daca8a593 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -431,6 +431,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Logging.Core.Integrati EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Logging.Core.UnitTests", "test\unit\Elsa.Logging.Core.UnitTests\Elsa.Logging.Core.UnitTests.csproj", "{4229B9B3-60D3-4CFE-B147-B3865212C6C8}" 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 @@ -1063,6 +1068,7 @@ Global {48A85A19-B654-4570-B332-653BC0B6A846} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} {A5C87AAF-E607-4DA7-B2E1-08FEAA41B293} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} {4229B9B3-60D3-4CFE-B147-B3865212C6C8} = {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} diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md new file mode 100644 index 000000000..dcacdf4ec --- /dev/null +++ b/doc/qa/test-guidelines.md @@ -0,0 +1,28 @@ +# Test Guidelines + +## Unit tests +All logic should be covered by unit tests. Unit tests should be isolated, fast, and deterministic. Use mocking frameworks to isolate dependencies. + +The desired code coverage is 100%. + +## dump + +Consider the following constraints to have the best possible way of testing of the elsa engine + +- Not be affected by execution times +- not having to depend on delays unless there is no other way +- how do we get the workflow definitions in the engine + - is the test responsible for publish + - can it be in bulk by an import + - can it work with docker, env, k8s cluster deployments + - how can we manage the tests version with the workflow definition version? +- what are steps that we need, the templates to not have repetitive implementations +- is the journal and activity execution endpoints the best to do the asserts upon. +- are there alternatives like querying the db +..... +- is execute the best to test the activities or a workflow with and http endpoint +- how to test failures , like the fail activity, no workflow instance returned no way to find the instance + - or by using a correlation id + - how to avoid get latest instance of a definition etc. + +the goal is to have a testing environment that is consistent in the execution. \ No newline at end of file From e41ed0cbf250e0415154216aa8bf981c9af8a6c0 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Fri, 26 Sep 2025 11:38:38 +0200 Subject: [PATCH 02/40] Updating initial document --- doc/qa/test-guidelines.md | 354 +++++++++++++++++++++++++++++++++++++- 1 file changed, 353 insertions(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index dcacdf4ec..87e50f154 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -25,4 +25,356 @@ Consider the following constraints to have the best possible way of testing of t - or by using a correlation id - how to avoid get latest instance of a definition etc. -the goal is to have a testing environment that is consistent in the execution. \ No newline at end of file +the goal is to have a testing environment that is consistent in the execution. + +--- +# Elsa Core — Test Strategy (Initial Draft) + +**Purpose:** +This document describes recommended testing strategies for the Elsa engine given the constraints you listed. It is written for architects and senior contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and end‑to‑end tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. + +--- + +## Goals / Non‑functional requirements + +1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of wall‑clock timing or transient delays. +2. **Fast feedback** — unit and integration tests should run quickly to support local workflows and CI. +3. **Minimal reliance on real delays** — avoid `Thread.Sleep` or real clocks except where unavoidable; prefer fakes or manual progress of time. +4. **Environment portability** — tests should run in local dev, Docker Compose and Kubernetes CI environments with minimal changes. +5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow blueprint version. +6. **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. +7. **Clear assertion points** — provide a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. +8. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. + +--- + +## High‑level testing pyramid for Elsa + +- **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. +- **Component/integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. +- **Contract tests**: Verify activity contracts and public APIs of activity packages (e.g. HTTP, Email, Messaging). Ensure a versioned contract for activities. +- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST or gRPC and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. + +--- + +## Key constraints & recommended patterns (mapped to your concerns) + +### 1) Not be affected by execution times / avoid delays + +- **Use a fake clock / testable time source**: Inject an `IClock` abstraction (if not present) or use Elsa's existing pluggable clock. In tests, provide a `FakeClock` whose time you advance programmatically. This eliminates real waiting for timer activities. + +- **Deterministic Bookmark Scheduling**: Replace the production scheduler with an in‑process deterministic scheduler in tests. Instead of scheduling real OS timers, the test scheduler stores due bookmarks and exposes a `TriggerNext()` or `TriggerAll()` method. + +- **Synchronous execution mode**: Provide a test helper that runs workflows synchronously to completion when possible (e.g. `TestWorkflowRunner.RunToCompletion(workflowDefinition, inputs)`) — this executes activities inline and returns once the workflow is idle or complete. Use this for most assertions instead of waiting for background workers. + +- **Avoid arbitrary sleeps**: If polling is necessary (e.g. for external system integration), use exponential backoff with short upper bounds and strong invariants (correlation ids) to detect test success quickly. + +- **Async completion waiters**: When invoking workflows through the HTTP /execute endpoint (which returns immediately), provide a helper such as WaitForCompletionAsync(instanceId) that polls the workflow instance state or subscribes to completion events. This replaces fragile Thread.Sleep patterns and ensures tests only assert once the workflow has actually finished. + + +### 2) Not having to depend on delays unless there is no other way + +- **Prefer manual triggers**: For timers or external events, design tests to call the engine's trigger API (e.g. raise signal, post message, call `ResumeBookmark`) rather than waiting for a timer to fire. + +- **Use time manipulation**: If testing timer semantics is required, advance the fake clock and then run the scheduler loop; avoid real-time waits. + +- **Event-driven assertions**: Subscribe to engine events (WorkflowCompleted, ActivityExecuted, etc.) in tests. Block until the event for the specific instance arrives instead of waiting arbitrary amounts of time. + +--- + +### Example: Event-driven completion helper + +Instead of polling, tests can subscribe to Elsa's event bus and await a specific event. This approach is faster and avoids unnecessary DB queries. + +```csharp +public static class WorkflowEventWaiter +{ + public static Task WaitForWorkflowCompletedAsync( + IEventPublisher publisher, + string instanceId, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + timeout ??= TimeSpan.FromSeconds(30); + + void Handler(WorkflowCompleted evt) + { + if (evt.WorkflowInstanceId == instanceId) + tcs.TrySetResult(evt); + } + + publisher.Subscribe(Handler); + + _ = Task.Delay(timeout.Value, cancellationToken) + .ContinueWith(_ => tcs.TrySetException(new TimeoutException($\"Workflow {instanceId} did not complete within {timeout}.\"))); + + return tcs.Task; + } +} +``` + +Usage in a test: + +```csharp +// Start workflow via /execute +var instanceId = response.InstanceId; + +// Wait for the event +var completedEvent = await WorkflowEventWaiter.WaitForWorkflowCompletedAsync(publisher, instanceId); +Assert.Equal(instanceId, completedEvent.WorkflowInstanceId); +``` + +This event-driven strategy ensures assertions only happen once Elsa signals the workflow is complete, making tests both fast and deterministic. + +--- + + +### 3) How do we get the workflow definitions into the engine and Is the test responsible for publish? + +Two main patterns: + +**A. Programmatic/Code‑first registration (recommended for unit/component tests)** +- Register workflows as code (C# fluent `IWorkflowBuilder`) inside test setup. This is fast and avoids serialization roundtrips. +- Use a lightweight in‑memory `IWorkflowRegistry` implementation for tests. +- Good for tests that validate runtime behavior without involving persistence or designer serialization. + +**B. Serialized definitions (recommended for integration & E2E tests)** +- Store JSON/YAML workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. +- Tests are responsible for *publishing* the definitions into the test host if the scenario requires persistence (e.g. testing versioned definitions or import behavior). +- For bulk tests, provide an import script (`ImportTestDefinitions.sh`) that uploads all artifacts in a single batch before running assertions. + +**Which to use?** +- Unit/component tests: code-first programmatic definitions. +- Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. + + +### 4) Bulk import + +- Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: + - Validate schema and version. + - Report conflicts or duplicate IDs. + - Run in parallel but enforce deterministic ordering when versions matter. +- For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. + + +### 5) Working with Docker, env, K8s cluster deployments + +- **Test strategy split**: + - Local developer tests: use in‑process hosts and in‑memory/ephemeral DBs (SQLite in-memory or Testcontainers-based DB). Fast and deterministic. + - CI Docker Compose: spin up a lightweight containerized environment with the host app, a real DB (Postgres, SQL Server or Mongo) and optional message broker; use Testcontainers (or Docker Compose) to orchestrate in CI. + - K8s E2E: run a small suite that deploys a test namespace with Helm or apply manifests. Use ephemeral resources and ensure cleanup. Keep these tests in a separate CI stage. + +- **Use Testcontainers** (or equivalent) to provision ephemeral DBs/brokers in CI; this keeps environments close to production while still being isolated and reproducible. + +- **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. + + +### 6) How can we manage the tests version with the workflow definition version? + +- **Source control the workflow artifacts** and apply semantic versioning to their filenames or metadata (e.g. `payment-process.v1.2.0.json`). +- **Test manifests**: each test (or test suite) references the exact artifact version it needs via a manifest file (e.g. `tests/manifests/payment-suite.json` lists definitions with versions). CI uses the manifest to import the right versions. +- **Immutable artifacts**: Do not overwrite a published artifact used by tests. If a workflow changes, publish a new version and update tests to point to the new artifact. +- **Automate snapshots**: On CI, capture the actual deployed workflow definition version (ID + version) and record it with test results for traceability. + + +### 7) Steps needed and templates to avoid repetition + +**Test lifecycle template (common for integration/E2E tests)** +1. **Provision** the environment (in‑process host or containerized test environment). +2. **Reset** persistence (drop / recreate DB schema or use a clean DB instance). +3. **Import/Publish** required workflow definitions (use code-first for unit tests). +4. **Register** test hooks (e.g. fake clock, fake scheduler, activity test doubles, callback endpoints). +5. **Invoke** the workflow via the API, direct invoker or trigger. +6. **Advance** time or trigger bookmarks manually if needed. +7. **Assert** via journal/events/DB/state. +8. **Tear down** environment and collect artifacts (logs, DB snapshots) on failures. + +**Reusable code artifacts** +- `TestHostFactory`: create and configure test hosts (DI container, fake services). +- `WorkflowDefinitionLoader`: loads definitions from disk, validates versions, and publishes them. +- `DeterministicScheduler`: test scheduler with `TriggerOnce(correlationId)`, `AdvanceTo(time)`. +- `ActivityTestProbe`: an in‑process activity wrapper that captures inputs/outputs and emits structured events to assert against. +- YAML/JSON manifest schema for suite imports. + +Include these helpers in a shared test utilities NuGet/package so all test projects can reuse them and reduce duplication. + + +### 8) Is the journal and activity execution endpoints the best place to assert upon? Alternatives? + +**Journal / Activity Execution Endpoints (Pros)** +- Journal provides a chronological, human‑readable trace of what happened and is close to production observability. +- Activity execution endpoints (if available) allow real API surface testing and validate telemetry and audit paths. + +**Cons** +- Journal may be high volume and require parsing to find relevant entries; tests risk being brittle if journal format changes. +- Accessing activity endpoints over HTTP introduces network flakiness in E2E tests. + +**Alternatives / Complementary options** +- **Direct DB queries**: Query the workflow instance table, bookmarks, and activity logs. Stronger for deterministic assertions about state (e.g. `WorkflowInstance.Status == Completed`). +- **Event stream / notifications**: Subscribe to internal events (in tests) via the mediator or a test `INotificationHandler` to assert lifecycle events as they happen in real time. +- **Activity test probes**: Instrument activities in tests to emit structured markers (test hooks) that are easier to assert than raw journal text. +- **Correlation IDs**: Always propagate and assert on correlation IDs attached to workflow instances and events to locate the exact instance you need. + +**Recommendation:** For unit and component tests, assert on in‑process events and test probes. For integration/E2E tests assert on durable state in the DB and validated events (or journal), and use correlation ids to make queries deterministic. Avoid relying solely on formatted journal lines. + + +### 9) Execute activity vs workflow with HTTP endpoint for tests + +- **Testing activities in isolation**: Unit test each activity class by constructing an `ActivityContext` and invoking `ExecuteAsync()` or using an `ActivityTestProbe`. This is the fastest and most isolated option. + +- **Testing activities in workflow**: Component tests should compose small workflows in code and run them through the `WorkflowInvoker` to validate end‑to‑end semantics (including variable passing, bookmarks, parallelism). Keep these tests in‑process to avoid network boundaries. + +- **Testing via HTTP**: Use HTTP endpoints for true E2E testing of the server host, middleware and serialization. These tests are slower and belong in the E2E suite. + +**Recommendation:** Unit test activities directly. Integration test the activity inside workflows with the in‑process invoker. Reserve HTTP‑based tests for full server behavior validation. + + +### 10) How to test failures (fail activity, missing instance, etc.) + +- **Explicit fail activities**: Unit test fail activities and assert they raise the expected error code and cause the workflow instance to transition to the appropriate state (e.g. Faulted). In integration tests, run the workflow to the failure point synchronously where possible and assert instance state. + +- **No workflow instance returned / missing instance**: + - Use correlation IDs passed at invocation time. The engine should return an `instanceId` or correlationId when starting. Tests should persist that id and query the instance store using it rather than using `GetLatest` semantics. + - If the engine design does not guarantee instance returns, wrap invocation in a test helper that extracts and returns the created instance id from either the API response, journal event, or DB entry. + +- **Correlated queries vs `GetLatest`**: Avoid `GetLatest` in tests because it is non‑deterministic in parallel runs. Use correlation ids, explicit instance IDs, or filters (workflow definition id + start time + unique test tag) to locate the exact instance. + +- **Simulating host failures**: In integration tests, simulate crashes by killing the host process/container mid‑execution and restarting it to validate persistence and resume semantics. Use persistent DB so state survives host restart. + + +### 11) Avoid `GetLatest` instance ambiguity + +- **Tag instances on creation**: Allow tests to send an explicit `TestCorrelationId` or `TestTag` as part of workflow input/metadata. Persist this tag to the instance record. Use it to query the DB deterministically. + +- **Return the instance id on start**: Ensure test harness captures the created instance id from the start API or in‑process invoker and uses that id for all subsequent queries. + + +### 12) Consistent execution environment + +- **Deterministic defaults**: For tests, use known configuration values (e.g. `MaxRetries=0`, `ShortCircuitLongRunning=true`) to eliminate production variability. +- **Isolated DB per test process**: Use ephemeral DBs (unique DB name per test run) to avoid cross‑test contamination. +- **Artifact collection**: On failure, collect logs, DB snapshot and exported journal to help triage flakiness. + +--- + +## Concrete examples & small templates + +> The repository should provide a `tests/test-utilities` project with the following helpers that are *reusable by all test projects*. + +### `TestHostFactory` (conceptual) + +```csharp +public static IHost CreateTestHost(Action configure) +{ + var host = Host.CreateDefaultBuilder() + .ConfigureServices((ctx, services) => + { + services.AddElsa(elsa => + { + // Use in-memory persistence and deterministic scheduler for tests + elsa.UseInMemoryPersistence(); + elsa.UseDeterministicScheduler(); + }); + + // Additional test overrides + configure?.Invoke(services); + }) + .Build(); + + host.Start(); + return host; +} +``` + +### `DeterministicScheduler` interface + +```csharp +public interface IDeterministicScheduler +{ + Task> GetDueBookmarksAsync(); + Task TriggerBookmarkAsync(string bookmarkId, string correlationId = null); + void AdvanceTo(DateTimeOffset time); +} +``` + +### Example test flow (integration, in‑process) + +1. Create host with `TestHostFactory` and `FakeClock`. +2. Load workflow definitions (code‑first or JSON loader). +3. Start workflow via `IWorkflowInvoker.StartAsync(definitionId, inputs, correlationId)` -> returns `instanceId`. +4. If workflow waits on bookmark, call `scheduler.TriggerBookmarkAsync(bookmarkId, correlationId)`. +5. Assert final instance status via `IWorkflowInstanceStore.FindById(instanceId)` or via `IEventCollector`. + + +### Example manifest (tests/manifests/payment-suite.json) + +```json +{ + "suiteName": "payment-suite", + "definitions": [ + { "id": "payment-process", "version": "1.2.0", "path": "definitions/payment-process.v1.2.0.json" }, + { "id": "refund-process", "version": "1.0.0", "path": "definitions/refund-process.v1.0.0.json" } + ] +} +``` + +--- + +## CI recommendations + +- **Local unit tests**: run in `dotnet test` step (fast). Use in‑memory stores and determinism. +- **Integration tests**: run with Testcontainers to provide real DB/broker. Run these in a separate CI job because they are slower. +- **K8s smoke tests**: optional separate stage. Deploy to ephemeral namespace via Helm and run a small suite of smoke E2E tests; tear down after. +- **Parallelization**: run multiple test matrices (DB providers) but avoid running heavy E2E jobs in parallel unless you have isolated resources. +- **Flaky test detection**: enable a flaky test retry policy for known non‑deterministic tests, but treat retries as signals to fix the underlying determinism problems. + +--- + +## Failure injection and resilience testing + +- **Deterministic fault injection**: provide test stubs for activities that throw predictable exceptions on demand. Use configuration flags or special test input to trigger them. +- **Host process kill**: in containerized tests, kill the host midway (Docker/kill or stop container) and restart to verify persistence/resumption. +- **Network partitions**: simulate by blocking network connections to DB/broker in the test environment to ensure graceful failure handling. + +--- + +## What to deliver in the repository + +- `tests/test-utilities/` project with common helpers (TestHostFactory, DeterministicScheduler, Test probes) +- `tests/definitions/` with versioned workflow artifacts +- `tests/manifests/` for suites referencing definitions and versions +- `tests/ci/docker-compose.yml` and `tests/ci/k8s/` manifests for reproducible integration/E2E runs +- Example tests showing patterns: + - Unit test for `HttpRequestActivity` (activity isolation) + - Integration test for workflow with timer (fake clock + deterministic scheduler) + - E2E test that deploys a host in Docker and asserts via DB queries and journal + +--- + +## Next steps / TODOs for the team + +1. **Add `IClock` and `IDeterministicScheduler` abstractions** (if not present) and implement test doubles. +2. **Create `tests/test-utilities` project** and convert a couple of existing tests to use it as examples. +3. **Define manifest schema** and commit a couple of versioned workflow definitions to `tests/definitions/`. +4. **Add one CI integration job** that uses Testcontainers to run the integration suite against Postgres and Mongo providers. +5. **Add telemetry and event collectors** to support in‑process assertions (easier than parsing journal text). + +--- + +## Appendix: Quick checklist for writing a new test + +- [ ] Will this be a unit, integration or E2E test? Choose minimal scope. +- [ ] Can we avoid real time? If yes, use fake clock/trigger. +- [ ] Will the test load a workflow definition artifact? Pin its version in the manifest. +- [ ] Will the test depend on DB state? Use a clean DB instance per test run. +- [ ] Use correlation ids for all invocations. +- [ ] Assert using deterministic state (instance id, DB record, or event) rather than `GetLatest`. +- [ ] On failure, capture logs and DB snapshot for diagnosis. + +--- + +**End of initial draft** + +Please tell me which parts you want expanded first (examples, code snippets for the scheduler, CI yaml, sample tests converted from existing tests in the repo, or a short checklist for reviewers), and I will extend this document accordingly. + From 57459da885de079902d151c35e0b3317b8ec62de Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Fri, 26 Sep 2025 15:16:08 +0200 Subject: [PATCH 03/40] Improving the test guidelines document --- doc/qa/test-guidelines.md | 123 +++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 68 deletions(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 87e50f154..4927ddeea 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -1,10 +1,5 @@ # Test Guidelines -## Unit tests -All logic should be covered by unit tests. Unit tests should be isolated, fast, and deterministic. Use mocking frameworks to isolate dependencies. - -The desired code coverage is 100%. - ## dump Consider the following constraints to have the best possible way of testing of the elsa engine @@ -28,19 +23,19 @@ Consider the following constraints to have the best possible way of testing of t the goal is to have a testing environment that is consistent in the execution. --- -# Elsa Core — Test Strategy (Initial Draft) +# Elsa Core — Test Strategy Guidelines **Purpose:** -This document describes recommended testing strategies for the Elsa engine given the constraints you listed. It is written for architects and senior contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and end‑to‑end tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. +This document describes recommended testing strategies for the Elsa engine. It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and end‑to‑end tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. --- ## Goals / Non‑functional requirements -1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of wall‑clock timing or transient delays. +1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. 2. **Fast feedback** — unit and integration tests should run quickly to support local workflows and CI. -3. **Minimal reliance on real delays** — avoid `Thread.Sleep` or real clocks except where unavoidable; prefer fakes or manual progress of time. -4. **Environment portability** — tests should run in local dev, Docker Compose and Kubernetes CI environments with minimal changes. +3. **Minimal reliance on real delays** — avoid `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. +4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. 5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow blueprint version. 6. **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. 7. **Clear assertion points** — provide a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. @@ -51,32 +46,26 @@ This document describes recommended testing strategies for the Elsa engine given ## High‑level testing pyramid for Elsa - **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. + - Use [xUnit / NUnit / MSTest] with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. - **Component/integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. + - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like JTest. - **Contract tests**: Verify activity contracts and public APIs of activity packages (e.g. HTTP, Email, Messaging). Ensure a versioned contract for activities. -- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST or gRPC and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. + - Use a shared test suite that activity package authors can run against their implementations. +- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. --- -## Key constraints & recommended patterns (mapped to your concerns) +## Key constraints & recommended patterns -### 1) Not be affected by execution times / avoid delays - -- **Use a fake clock / testable time source**: Inject an `IClock` abstraction (if not present) or use Elsa's existing pluggable clock. In tests, provide a `FakeClock` whose time you advance programmatically. This eliminates real waiting for timer activities. - -- **Deterministic Bookmark Scheduling**: Replace the production scheduler with an in‑process deterministic scheduler in tests. Instead of scheduling real OS timers, the test scheduler stores due bookmarks and exposes a `TriggerNext()` or `TriggerAll()` method. +### 1. Should not be affected by execution times / Not depend on delays unless there is no other way - **Synchronous execution mode**: Provide a test helper that runs workflows synchronously to completion when possible (e.g. `TestWorkflowRunner.RunToCompletion(workflowDefinition, inputs)`) — this executes activities inline and returns once the workflow is idle or complete. Use this for most assertions instead of waiting for background workers. - **Avoid arbitrary sleeps**: If polling is necessary (e.g. for external system integration), use exponential backoff with short upper bounds and strong invariants (correlation ids) to detect test success quickly. -- **Async completion waiters**: When invoking workflows through the HTTP /execute endpoint (which returns immediately), provide a helper such as WaitForCompletionAsync(instanceId) that polls the workflow instance state or subscribes to completion events. This replaces fragile Thread.Sleep patterns and ensures tests only assert once the workflow has actually finished. - - -### 2) Not having to depend on delays unless there is no other way - - **Prefer manual triggers**: For timers or external events, design tests to call the engine's trigger API (e.g. raise signal, post message, call `ResumeBookmark`) rather than waiting for a timer to fire. -- **Use time manipulation**: If testing timer semantics is required, advance the fake clock and then run the scheduler loop; avoid real-time waits. +- **Async completion waiters**: When invoking workflows through the HTTP `/execute` endpoint (which returns immediately), provide a helper, such as `WaitForCompletionAsync(instanceId)` (example below) that subscribes to completion events. This replaces fragile `Thread.Sleep` patterns and ensures tests only assert once the workflow has actually finished. - **Event-driven assertions**: Subscribe to engine events (WorkflowCompleted, ActivityExecuted, etc.) in tests. Block until the event for the specific instance arrives instead of waiting arbitrary amounts of time. @@ -84,7 +73,7 @@ This document describes recommended testing strategies for the Elsa engine given ### Example: Event-driven completion helper -Instead of polling, tests can subscribe to Elsa's event bus and await a specific event. This approach is faster and avoids unnecessary DB queries. +Tests can subscribe to Elsa's event bus and await a specific event. ```csharp public static class WorkflowEventWaiter @@ -129,12 +118,11 @@ This event-driven strategy ensures assertions only happen once Elsa signals the --- - -### 3) How do we get the workflow definitions into the engine and Is the test responsible for publish? +### 2. Importing and publishing workflow definitions for testing workflows Two main patterns: -**A. Programmatic/Code‑first registration (recommended for unit/component tests)** +**A. Programmatic/Code‑first registration (recommended for component tests)** - Register workflows as code (C# fluent `IWorkflowBuilder`) inside test setup. This is fast and avoids serialization roundtrips. - Use a lightweight in‑memory `IWorkflowRegistry` implementation for tests. - Good for tests that validate runtime behavior without involving persistence or designer serialization. @@ -145,11 +133,11 @@ Two main patterns: - For bulk tests, provide an import script (`ImportTestDefinitions.sh`) that uploads all artifacts in a single batch before running assertions. **Which to use?** -- Unit/component tests: code-first programmatic definitions. +- Component tests: code-first programmatic definitions. - Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. -### 4) Bulk import +### 3. Bulk import - Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: - Validate schema and version. @@ -158,7 +146,7 @@ Two main patterns: - For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. -### 5) Working with Docker, env, K8s cluster deployments +### 4. Working with Docker, env, K8s cluster deployments - **Test strategy split**: - Local developer tests: use in‑process hosts and in‑memory/ephemeral DBs (SQLite in-memory or Testcontainers-based DB). Fast and deterministic. @@ -170,39 +158,40 @@ Two main patterns: - **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. -### 6) How can we manage the tests version with the workflow definition version? +### 5. Managing test and workflow definition versions - **Source control the workflow artifacts** and apply semantic versioning to their filenames or metadata (e.g. `payment-process.v1.2.0.json`). -- **Test manifests**: each test (or test suite) references the exact artifact version it needs via a manifest file (e.g. `tests/manifests/payment-suite.json` lists definitions with versions). CI uses the manifest to import the right versions. - **Immutable artifacts**: Do not overwrite a published artifact used by tests. If a workflow changes, publish a new version and update tests to point to the new artifact. - **Automate snapshots**: On CI, capture the actual deployed workflow definition version (ID + version) and record it with test results for traceability. -### 7) Steps needed and templates to avoid repetition +### 6. Avoiding repetition **Test lifecycle template (common for integration/E2E tests)** 1. **Provision** the environment (in‑process host or containerized test environment). 2. **Reset** persistence (drop / recreate DB schema or use a clean DB instance). -3. **Import/Publish** required workflow definitions (use code-first for unit tests). -4. **Register** test hooks (e.g. fake clock, fake scheduler, activity test doubles, callback endpoints). +3. **Import/Publish** required workflow definitions. +4. **Register** test hooks (e.g. fake scheduler, activity test doubles, callback endpoints). 5. **Invoke** the workflow via the API, direct invoker or trigger. -6. **Advance** time or trigger bookmarks manually if needed. +6. **Trigger** bookmarks manually if needed. 7. **Assert** via journal/events/DB/state. 8. **Tear down** environment and collect artifacts (logs, DB snapshots) on failures. **Reusable code artifacts** - `TestHostFactory`: create and configure test hosts (DI container, fake services). - `WorkflowDefinitionLoader`: loads definitions from disk, validates versions, and publishes them. -- `DeterministicScheduler`: test scheduler with `TriggerOnce(correlationId)`, `AdvanceTo(time)`. +- `DeterministicScheduler`: test scheduler with `TriggerOnce(correlationId)`. - `ActivityTestProbe`: an in‑process activity wrapper that captures inputs/outputs and emits structured events to assert against. -- YAML/JSON manifest schema for suite imports. +- JSON manifest schema for suite imports. Include these helpers in a shared test utilities NuGet/package so all test projects can reuse them and reduce duplication. -### 8) Is the journal and activity execution endpoints the best place to assert upon? Alternatives? +### 7. Assertion targets and alternatives -**Journal / Activity Execution Endpoints (Pros)** +#### Journal / Activity Execution Endpoints + +**Pros** - Journal provides a chronological, human‑readable trace of what happened and is close to production observability. - Activity execution endpoints (if available) allow real API surface testing and validate telemetry and audit paths. @@ -210,47 +199,50 @@ Include these helpers in a shared test utilities NuGet/package so all test proje - Journal may be high volume and require parsing to find relevant entries; tests risk being brittle if journal format changes. - Accessing activity endpoints over HTTP introduces network flakiness in E2E tests. -**Alternatives / Complementary options** +#### Alternatives / Complementary options - **Direct DB queries**: Query the workflow instance table, bookmarks, and activity logs. Stronger for deterministic assertions about state (e.g. `WorkflowInstance.Status == Completed`). - **Event stream / notifications**: Subscribe to internal events (in tests) via the mediator or a test `INotificationHandler` to assert lifecycle events as they happen in real time. - **Activity test probes**: Instrument activities in tests to emit structured markers (test hooks) that are easier to assert than raw journal text. -- **Correlation IDs**: Always propagate and assert on correlation IDs attached to workflow instances and events to locate the exact instance you need. -**Recommendation:** For unit and component tests, assert on in‑process events and test probes. For integration/E2E tests assert on durable state in the DB and validated events (or journal), and use correlation ids to make queries deterministic. Avoid relying solely on formatted journal lines. +**Recommendations:** +- For unit and component tests, assert on in‑process events and test probes. For integration/E2E tests assert on durable state in the DB and validated events (or journal), and use correlation ids to make queries deterministic. Avoid relying solely on formatted journal lines. +- Always propagate and assert on correlation IDs attached to workflow instances and events to locate the exact instance you need. -### 9) Execute activity vs workflow with HTTP endpoint for tests +### 8. Execute endpoint vs HTTP activity for tests - **Testing activities in isolation**: Unit test each activity class by constructing an `ActivityContext` and invoking `ExecuteAsync()` or using an `ActivityTestProbe`. This is the fastest and most isolated option. - **Testing activities in workflow**: Component tests should compose small workflows in code and run them through the `WorkflowInvoker` to validate end‑to‑end semantics (including variable passing, bookmarks, parallelism). Keep these tests in‑process to avoid network boundaries. -- **Testing via HTTP**: Use HTTP endpoints for true E2E testing of the server host, middleware and serialization. These tests are slower and belong in the E2E suite. +- **Testing via HTTP**: Use HTTP endpoint activities for integration and true E2E testing of the server host, middleware and serialization. These tests are slower and belong in the E2E suite. There are two possibilities: + - Workflow with the desired activity and a connected entry HTTP endpoint activity; + - `/execute` endpoint that runs the workflow directly, in this case, the HTTP endpoint activity is not necessary. -**Recommendation:** Unit test activities directly. Integration test the activity inside workflows with the in‑process invoker. Reserve HTTP‑based tests for full server behavior validation. +**Recommendation:** Unit test activities directly. Integration test the activity inside workflows with the in‑process invoker. Reserve HTTP‑based tests for integrations and full server behavior validation. -### 10) How to test failures (fail activity, missing instance, etc.) +### 9. How to test failures (fail activity, missing instance, etc.) - **Explicit fail activities**: Unit test fail activities and assert they raise the expected error code and cause the workflow instance to transition to the appropriate state (e.g. Faulted). In integration tests, run the workflow to the failure point synchronously where possible and assert instance state. - **No workflow instance returned / missing instance**: - - Use correlation IDs passed at invocation time. The engine should return an `instanceId` or correlationId when starting. Tests should persist that id and query the instance store using it rather than using `GetLatest` semantics. - - If the engine design does not guarantee instance returns, wrap invocation in a test helper that extracts and returns the created instance id from either the API response, journal event, or DB entry. + - Use correlation IDs passed at invocation time. The engine should return an `instanceId` or `correlationId` when starting. Tests should persist that id and query the instance store using it rather than using `GetLatest` semantics. + - If there are no guaranteed instance returns, wrap invocation in a test helper that extracts and returns the created instance id from either the API response, journal event, or DB entry. -- **Correlated queries vs `GetLatest`**: Avoid `GetLatest` in tests because it is non‑deterministic in parallel runs. Use correlation ids, explicit instance IDs, or filters (workflow definition id + start time + unique test tag) to locate the exact instance. +- **Correlated queries vs GetLatest**: Avoid `GetLatest` in tests because it is non‑deterministic in parallel runs. Use correlation ids, explicit instance IDs, or filters (workflow definition id + start time + unique test tag) to locate the exact instance. - **Simulating host failures**: In integration tests, simulate crashes by killing the host process/container mid‑execution and restarting it to validate persistence and resume semantics. Use persistent DB so state survives host restart. -### 11) Avoid `GetLatest` instance ambiguity +### 10. Avoid instance ambiguity - **Tag instances on creation**: Allow tests to send an explicit `TestCorrelationId` or `TestTag` as part of workflow input/metadata. Persist this tag to the instance record. Use it to query the DB deterministically. - **Return the instance id on start**: Ensure test harness captures the created instance id from the start API or in‑process invoker and uses that id for all subsequent queries. -### 12) Consistent execution environment +### 11. Consistent execution environment - **Deterministic defaults**: For tests, use known configuration values (e.g. `MaxRetries=0`, `ShortCircuitLongRunning=true`) to eliminate production variability. - **Isolated DB per test process**: Use ephemeral DBs (unique DB name per test run) to avoid cross‑test contamination. @@ -294,27 +286,26 @@ public interface IDeterministicScheduler { Task> GetDueBookmarksAsync(); Task TriggerBookmarkAsync(string bookmarkId, string correlationId = null); - void AdvanceTo(DateTimeOffset time); } ``` ### Example test flow (integration, in‑process) -1. Create host with `TestHostFactory` and `FakeClock`. +1. Create host with `TestHostFactory`. 2. Load workflow definitions (code‑first or JSON loader). 3. Start workflow via `IWorkflowInvoker.StartAsync(definitionId, inputs, correlationId)` -> returns `instanceId`. 4. If workflow waits on bookmark, call `scheduler.TriggerBookmarkAsync(bookmarkId, correlationId)`. 5. Assert final instance status via `IWorkflowInstanceStore.FindById(instanceId)` or via `IEventCollector`. -### Example manifest (tests/manifests/payment-suite.json) +### Example manifest (tests/manifests/decision.json) ```json { - "suiteName": "payment-suite", + "suiteName": "decision", "definitions": [ - { "id": "payment-process", "version": "1.2.0", "path": "definitions/payment-process.v1.2.0.json" }, - { "id": "refund-process", "version": "1.0.0", "path": "definitions/refund-process.v1.0.0.json" } + { "id": "decision-true", "version": "1.2.0", "path": "definitions/decision-true.v1.2.0.json" }, + { "id": "decision-true", "version": "1.0.0", "path": "definitions/decision-false.v1.0.0.json" } ] } ``` @@ -324,7 +315,7 @@ public interface IDeterministicScheduler ## CI recommendations - **Local unit tests**: run in `dotnet test` step (fast). Use in‑memory stores and determinism. -- **Integration tests**: run with Testcontainers to provide real DB/broker. Run these in a separate CI job because they are slower. +- **Integration tests**: run with Testcontainers (or similar) to provide real DB/broker. Run these in a separate CI job because they are slower. - **K8s smoke tests**: optional separate stage. Deploy to ephemeral namespace via Helm and run a small suite of smoke E2E tests; tear down after. - **Parallelization**: run multiple test matrices (DB providers) but avoid running heavy E2E jobs in parallel unless you have isolated resources. - **Flaky test detection**: enable a flaky test retry policy for known non‑deterministic tests, but treat retries as signals to fix the underlying determinism problems. @@ -347,25 +338,25 @@ public interface IDeterministicScheduler - `tests/ci/docker-compose.yml` and `tests/ci/k8s/` manifests for reproducible integration/E2E runs - Example tests showing patterns: - Unit test for `HttpRequestActivity` (activity isolation) - - Integration test for workflow with timer (fake clock + deterministic scheduler) + - Integration test for basic workflow (deterministic scheduler) - E2E test that deploys a host in Docker and asserts via DB queries and journal --- ## Next steps / TODOs for the team -1. **Add `IClock` and `IDeterministicScheduler` abstractions** (if not present) and implement test doubles. +1. **Increase** unit test coverage of existing code using these patterns. +2. **Add `IDeterministicScheduler` abstractions** and implement test doubles. 2. **Create `tests/test-utilities` project** and convert a couple of existing tests to use it as examples. 3. **Define manifest schema** and commit a couple of versioned workflow definitions to `tests/definitions/`. -4. **Add one CI integration job** that uses Testcontainers to run the integration suite against Postgres and Mongo providers. +4. **Add one CI integration job** that uses Testcontainers (or similar) to run the integration suite against popular db providers (MySql, Postgres and Mongo, for example). 5. **Add telemetry and event collectors** to support in‑process assertions (easier than parsing journal text). - --- ## Appendix: Quick checklist for writing a new test - [ ] Will this be a unit, integration or E2E test? Choose minimal scope. -- [ ] Can we avoid real time? If yes, use fake clock/trigger. +- [ ] Can we avoid real time? If yes, use fake trigger. - [ ] Will the test load a workflow definition artifact? Pin its version in the manifest. - [ ] Will the test depend on DB state? Use a clean DB instance per test run. - [ ] Use correlation ids for all invocations. @@ -374,7 +365,3 @@ public interface IDeterministicScheduler --- -**End of initial draft** - -Please tell me which parts you want expanded first (examples, code snippets for the scheduler, CI yaml, sample tests converted from existing tests in the repo, or a short checklist for reviewers), and I will extend this document accordingly. - From 96f2d1d23a45b0ceb3ba01a260bb5d1261fc5a5e Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Fri, 26 Sep 2025 15:17:03 +0200 Subject: [PATCH 04/40] Removing bogus information from test-guidelines.md --- doc/qa/test-guidelines.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 4927ddeea..449276b4a 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -1,28 +1,3 @@ -# Test Guidelines - -## dump - -Consider the following constraints to have the best possible way of testing of the elsa engine - -- Not be affected by execution times -- not having to depend on delays unless there is no other way -- how do we get the workflow definitions in the engine - - is the test responsible for publish - - can it be in bulk by an import - - can it work with docker, env, k8s cluster deployments - - how can we manage the tests version with the workflow definition version? -- what are steps that we need, the templates to not have repetitive implementations -- is the journal and activity execution endpoints the best to do the asserts upon. -- are there alternatives like querying the db -..... -- is execute the best to test the activities or a workflow with and http endpoint -- how to test failures , like the fail activity, no workflow instance returned no way to find the instance - - or by using a correlation id - - how to avoid get latest instance of a definition etc. - -the goal is to have a testing environment that is consistent in the execution. - ---- # Elsa Core — Test Strategy Guidelines **Purpose:** From a4e830ac540118888751398cd9e4c84302084266 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 29 Sep 2025 09:49:37 +0200 Subject: [PATCH 05/40] Reverting some changes for 3.6.0 compatibility --- Elsa.sln | 15 -------- src/apps/Elsa.Studio.Web/appsettings.json | 15 -------- .../VariableTypeDefinitionProvider.cs | 24 ++++++------ .../TypeDefinitionDocumentRenderer.cs | 2 +- .../TypeDefinitions/Services/TypeDescriber.cs | 6 +-- .../Elsa.Http/Activities/SendHttpRequest.cs | 2 - src/modules/Elsa.Http/Features/HttpFeature.cs | 2 +- .../SendHttpRequestActivityPortResolver.cs | 4 +- .../Endpoints/Bookmarks/Resume/Endpoint.cs | 38 +++++-------------- .../Exceptions/InputEvaluationException.cs | 6 --- ...cutionContextExtensions.InputEvaluation.cs | 21 ++-------- 11 files changed, 30 insertions(+), 105 deletions(-) delete mode 100644 src/apps/Elsa.Studio.Web/appsettings.json delete mode 100644 src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs diff --git a/Elsa.sln b/Elsa.sln index 1bc1e2d34..dd8b561bb 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -699,21 +699,6 @@ Global {71D5178D-2490-4681-8621-BF8DED964F33} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {698051E0-7981-43D4-B7BA-F3D8B65004A1} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {51C39AF0-4F41-4FC1-AEBD-D1494407D3F9} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {70593549-8B26-4D63-9857-6BA8BB3E31DB} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} - {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} - {7FD1FD1E-5778-4065-AAA5-1F878129EF77} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} - {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} - {C583AF05-D517-4B7F-8955-6B61500ED3D8} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} - {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {68A0BC44-8A3E-4C45-8AFF-0662B81D2739} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} - {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} - {2CE3BD1E-0966-47DF-B870-6A4EB7EA0188} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} - {3E6DFD22-5F71-4A4E-A792-B010ADDCFBA6} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} - {48A85A19-B654-4570-B332-653BC0B6A846} = {1FB2FE77-5D7F-48D5-8FFE-530D21AFBA7D} - {A5C87AAF-E607-4DA7-B2E1-08FEAA41B293} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {4229B9B3-60D3-4CFE-B147-B3865212C6C8} = {18453B51-25EB-4317-A4B3-B10518252E92} {0478E6EA-DCB2-4667-ADC2-37C62C9C2574} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/apps/Elsa.Studio.Web/appsettings.json b/src/apps/Elsa.Studio.Web/appsettings.json deleted file mode 100644 index b617c6e56..000000000 --- a/src/apps/Elsa.Studio.Web/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, - "ElsaServer": { - "Url": "https://localhost:5001/elsa/api" - }, - "Hosting": { - "BasePath": "" - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs index f2d5a8b42..60bc39e8c 100644 --- a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs +++ b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs @@ -9,28 +9,28 @@ namespace Elsa.Expressions.JavaScript.Providers; /// /// Produces s for variable types. /// -[UsedImplicitly] -internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber, IOptions options) : TypeDefinitionProvider +internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber) : TypeDefinitionProvider { protected override IEnumerable GetTypeDefinitions(TypeDefinitionContext context) { var excludedTypes = new Func[] { type => type == typeof(ExpandoObject), - type => type.IsPrimitive, - type => type.ContainsGenericParameters, - type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>), - type => type == typeof(object), - type => type == typeof(string) + type => typeof(IDictionary).IsAssignableFrom(type), + type => type == typeof(object) }; - var variableTypes = - from variableDescriptor in options.Value.VariableDescriptors - let variableType = variableDescriptor.Type - where (variableType.IsClass || variableType.IsInterface || variableType.IsEnum) && !excludedTypes.Any(x => x(variableType)) + var variables = context.WorkflowGraph.Workflow.Variables; + + var variableTypeQuery = + from variable in variables + let variableType = variable.GetVariableType() + where (variableType.IsClass || variableType.IsInterface || variableType.IsEnum) && !variableType.IsPrimitive && !excludedTypes.Any(x => x(variableType)) select variableType; - foreach (var variableType in variableTypes.Distinct()) + var variableTypes = variableTypeQuery.Distinct(); + + foreach (var variableType in variableTypes) { yield return typeDescriber.DescribeType(variableType); } diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs index cbd84d69d..fcc66000a 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs @@ -58,7 +58,7 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer } private void Render(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name}{(property.IsOptional ? "?" : "")}: {property.Type};"); - private void RenderEnumMember(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name} = \"{property.Name}\","); + private void RenderEnumMember(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name} = \"{property.Name}\";"); private void Render(VariableDefinition variable, StringBuilder output) => output.AppendLine($"declare var {variable.Name}: {variable.Type};"); string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}"; string RenderParameters(IEnumerable parameters) => string.Join(", ", parameters.Select(RenderParameter)); diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs index ef2a395a1..fc7079e98 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.CompilerServices; using Elsa.Extensions; using Elsa.Expressions.JavaScript.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts; @@ -41,10 +40,7 @@ public class TypeDescriber : ITypeDescriber yield break; #pragma warning disable IL2070 - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) - .Where(x => !x.IsSpecialName) - .Where(x => x.GetCustomAttribute() == null) - .ToList(); + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => !x.IsSpecialName).ToList(); #pragma warning restore IL2070 foreach (var method in methods) diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index e0a564259..7b27571c4 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -33,13 +33,11 @@ public class SendHttpRequest : SendHttpRequestBase /// /// The activity to execute when the HTTP request fails to connect. /// - [Port] public IActivity? FailedToConnect { get; set; } /// /// The activity to execute when the HTTP request times out. /// - [Port] public IActivity? Timeout { get; set; } /// diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 8ced09389..322e6dfc4 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -75,7 +75,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) }; /// - /// A delegate to configure the used when by the and activities. + /// A delegate to configure the used when by the activity. /// public Action HttpClient { get; set; } = (_, _) => { }; diff --git a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs index 82a3b91c2..92dcd991d 100644 --- a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs +++ b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs @@ -18,8 +18,8 @@ public class SendHttpRequestActivityResolver : IActivityResolver /// public ValueTask> GetActivityPortsAsync(IActivity activity, CancellationToken cancellationToken = default) { - var ports = GetPortsInternal(activity); - return new(ports); + IEnumerable ports = GetPortsInternal(activity); + return new ValueTask>(ports); } private IEnumerable GetPortsInternal(IActivity activity) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs index 0943545f0..832b60f59 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs @@ -11,7 +11,7 @@ namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume; /// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token. /// [PublicAPI] -internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResumer, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint +internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint { /// public override void Configure() @@ -25,7 +25,6 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { var token = Query("t")!; - var asynchronous = Query("async", false); if (!tokenService.TryDecryptToken(token, out var payload)) AddError("Invalid token."); @@ -38,11 +37,7 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum return; } - // Some clients, like Blazor, may prematurely cancel their request upon navigation away from the page. - // In this case, we don't want to cancel the workflow execution. - // We need to better understand the conditions that cause this. - var workflowCancellationToken = CancellationToken.None; - await ResumeBookmarkedWorkflowAsync(payload, input, asynchronous, workflowCancellationToken); + await ResumeBookmarkedWorkflowAsync(payload, input, cancellationToken); if (!HttpContext.Response.HasStarted) await Send.OkAsync(cancellationToken); @@ -65,35 +60,20 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum } } - private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary? input, bool asynchronous, CancellationToken cancellationToken) + private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary? input, CancellationToken cancellationToken) { var bookmarkId = tokenPayload.BookmarkId; var workflowInstanceId = tokenPayload.WorkflowInstanceId; - - if (asynchronous) - { - var item = new NewBookmarkQueueItem - { - BookmarkId = bookmarkId, - WorkflowInstanceId = workflowInstanceId, - Options = new() - { - Input = input - } - }; - - await bookmarkQueue.EnqueueAsync(item, cancellationToken); - return; - } - - var resumeRequest = new ResumeBookmarkRequest + var item = new NewBookmarkQueueItem { BookmarkId = bookmarkId, WorkflowInstanceId = workflowInstanceId, - Input = input + Options = new() + { + Input = input + } }; - - await workflowResumer.ResumeAsync(resumeRequest, cancellationToken); + await bookmarkQueue.EnqueueAsync(item, cancellationToken); } } diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs deleted file mode 100644 index b067f53e2..000000000 --- a/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Workflows.Exceptions; - -public class InputEvaluationException(string inputName, string message, Exception exception) : Exception(message, exception) -{ - public string InputName { get; } = inputName; -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index aee9509a3..35eed3f98 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -3,7 +3,6 @@ using Elsa.Expressions.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Workflows; -using Elsa.Workflows.Exceptions; using Elsa.Workflows.Models; // ReSharper disable once CheckNamespace @@ -67,20 +66,8 @@ public static partial class ActivityExecutionContextExtensions memoryBlockReference.Set(context, value); return value; } - + private static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) - { - try - { - return await EvaluateInputPropertyCoreAsync(context, activityDescriptor, inputDescriptor); - } - catch (Exception e) - { - throw new InputEvaluationException(inputDescriptor.Name, $"Failed to evaluate activity input '{inputDescriptor.Name}'", e); - } - } - - private static async Task EvaluateInputPropertyCoreAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { var activity = context.Activity; var defaultValue = inputDescriptor.DefaultValue; @@ -121,9 +108,9 @@ public static partial class ActivityExecutionContextExtensions if (memoryReference != null) { // When input is created from an activity provider, there may be no memory block reference ID. - if (memoryReference.Id == null!) + if (memoryReference.Id == null!) memoryReference.Id = $"{activity.NodeId}.{inputDescriptor.Name}"; // Construct a deterministic ID. - + // Declare the input memory block in the current context. context.ExpressionExecutionContext.Set(memoryReference, value!); } @@ -137,7 +124,7 @@ public static partial class ActivityExecutionContextExtensions return value; } - + private static Task StoreInputValueAsync(ActivityExecutionContext context, InputDescriptor inputDescriptor, object value) { // Store the serialized input value in the activity state. From ba9e2fef2029b061610c68720078b2ca065e33f7 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 29 Sep 2025 09:51:50 +0200 Subject: [PATCH 06/40] 3.6.0 compatibility --- .../ActivityExecutionContextExtensions.InputEvaluation.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index 35eed3f98..994601ab8 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -66,7 +66,6 @@ public static partial class ActivityExecutionContextExtensions memoryBlockReference.Set(context, value); return value; } - private static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { var activity = context.Activity; @@ -110,7 +109,6 @@ public static partial class ActivityExecutionContextExtensions // When input is created from an activity provider, there may be no memory block reference ID. if (memoryReference.Id == null!) memoryReference.Id = $"{activity.NodeId}.{inputDescriptor.Name}"; // Construct a deterministic ID. - // Declare the input memory block in the current context. context.ExpressionExecutionContext.Set(memoryReference, value!); } @@ -124,7 +122,6 @@ public static partial class ActivityExecutionContextExtensions return value; } - private static Task StoreInputValueAsync(ActivityExecutionContext context, InputDescriptor inputDescriptor, object value) { // Store the serialized input value in the activity state. From c7762a4b46ff402c9db42133ce24f1e43466054f Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 29 Sep 2025 09:58:43 +0200 Subject: [PATCH 07/40] 3.6.0 compatibility --- .../ActivityExecutionContextExtensions.InputEvaluation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index 994601ab8..093ede7f1 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -66,6 +66,7 @@ public static partial class ActivityExecutionContextExtensions memoryBlockReference.Set(context, value); return value; } + private static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { var activity = context.Activity; @@ -107,8 +108,9 @@ public static partial class ActivityExecutionContextExtensions if (memoryReference != null) { // When input is created from an activity provider, there may be no memory block reference ID. - if (memoryReference.Id == null!) + if (memoryReference.Id == null!) memoryReference.Id = $"{activity.NodeId}.{inputDescriptor.Name}"; // Construct a deterministic ID. + // Declare the input memory block in the current context. context.ExpressionExecutionContext.Set(memoryReference, value!); } @@ -122,6 +124,7 @@ public static partial class ActivityExecutionContextExtensions return value; } + private static Task StoreInputValueAsync(ActivityExecutionContext context, InputDescriptor inputDescriptor, object value) { // Store the serialized input value in the activity state. From f9b5ab7789739fd6824c2409bb28dc3e9fbf8def Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 29 Sep 2025 19:53:54 +0200 Subject: [PATCH 08/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- 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 449276b4a..831c3f6ba 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -9,7 +9,7 @@ This document describes recommended testing strategies for the Elsa engine. It i 1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. 2. **Fast feedback** — unit and integration tests should run quickly to support local workflows and CI. -3. **Minimal reliance on real delays** — avoid `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. +3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. 4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. 5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow blueprint version. 6. **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. From 40a6d59a4420fb4bc2338cdfc81a857b904ab408 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 29 Sep 2025 19:54:08 +0200 Subject: [PATCH 09/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- 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 831c3f6ba..47afc27de 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -8,7 +8,7 @@ This document describes recommended testing strategies for the Elsa engine. It i ## Goals / Non‑functional requirements 1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. -2. **Fast feedback** — unit and integration tests should run quickly to support local workflows and CI. +2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. 3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. 4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. 5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow blueprint version. From 1ad461f602c6bede11046dc0636db5f4f94c6350 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 29 Sep 2025 19:54:18 +0200 Subject: [PATCH 10/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- 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 47afc27de..c5b142452 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -11,7 +11,7 @@ This document describes recommended testing strategies for the Elsa engine. It i 2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. 3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. 4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. -5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow blueprint version. +5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. 6. **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. 7. **Clear assertion points** — provide a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. 8. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. From 260680f3a295bcb1e7f3a8034097bc6b0e879124 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 29 Sep 2025 19:55:30 +0200 Subject: [PATCH 11/40] Renaming test guideline --- Elsa.sln | 2 +- doc/qa/{test-guidelines.md => test-guidelines-collaborators.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename doc/qa/{test-guidelines.md => test-guidelines-collaborators.md} (99%) diff --git a/Elsa.sln b/Elsa.sln index dd8b561bb..1e9494b7d 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -299,7 +299,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.Integratio 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 + doc\qa\test-guidelines-collaborators.md = doc\qa\test-guidelines-collaborators.md EndProjectSection EndProject Global diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines-collaborators.md similarity index 99% rename from doc/qa/test-guidelines.md rename to doc/qa/test-guidelines-collaborators.md index 449276b4a..06e4addc8 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -1,4 +1,4 @@ -# Elsa Core — Test Strategy Guidelines +# Elsa Core — Test Strategy Guidelines for Collaborators **Purpose:** This document describes recommended testing strategies for the Elsa engine. It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and end‑to‑end tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. From 0041d13f3c30451e45f6d2cf2dfc99400cc5a332 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 29 Sep 2025 20:08:19 +0200 Subject: [PATCH 12/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 2f451d47a..1a36f0e70 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -20,7 +20,7 @@ This document describes recommended testing strategies for the Elsa engine. It i ## High‑level testing pyramid for Elsa -- **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. +- **Unit tests**: Activity logic, expression evaluators, services, providers, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - Use [xUnit / NUnit / MSTest] with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. - **Component/integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like JTest. From a201d99f7c21849dd43d92dd28671730871e92a5 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 29 Sep 2025 20:18:47 +0200 Subject: [PATCH 13/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 1a36f0e70..0b1549462 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -23,7 +23,7 @@ This document describes recommended testing strategies for the Elsa engine. It i - **Unit tests**: Activity logic, expression evaluators, services, providers, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - Use [xUnit / NUnit / MSTest] with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. - **Component/integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. - - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like JTest. + - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). - **Contract tests**: Verify activity contracts and public APIs of activity packages (e.g. HTTP, Email, Messaging). Ensure a versioned contract for activities. - Use a shared test suite that activity package authors can run against their implementations. - **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. From 4d78197c808b7496b0c40dc373fe3e3371f0871e Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 30 Sep 2025 08:59:44 +0200 Subject: [PATCH 14/40] Moving integrators instructions to another doc --- Elsa.sln | 1 + doc/qa/test-guidelines-collaborators.md | 17 ++++++++--------- doc/qa/test-guidelines-integrators.md | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 doc/qa/test-guidelines-integrators.md diff --git a/Elsa.sln b/Elsa.sln index 1e9494b7d..eb2fc7827 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -300,6 +300,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-4667-ADC2-37C62C9C2574}" ProjectSection(SolutionItems) = preProject doc\qa\test-guidelines-collaborators.md = doc\qa\test-guidelines-collaborators.md + doc\qa\test-guidelines-integrators.md = doc\qa\test-guidelines-integrators.md EndProjectSection EndProject Global diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 2f451d47a..50d5df90f 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -1,7 +1,9 @@ # Elsa Core — Test Strategy Guidelines for Collaborators **Purpose:** -This document describes recommended testing strategies for the Elsa engine. It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and end‑to‑end tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. +This document describes recommended testing strategies for the Elsa engine. +It is written for collaborators and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. +Additionally, it provides a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. --- @@ -12,21 +14,18 @@ This document describes recommended testing strategies for the Elsa engine. It i 3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. 4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. 5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. -6. **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. -7. **Clear assertion points** — provide a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. -8. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. +6. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. --- ## High‑level testing pyramid for Elsa - **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - - Use [xUnit / NUnit / MSTest] with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. -- **Component/integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. + - Use **xUnit** with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. +- **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like JTest. -- **Contract tests**: Verify activity contracts and public APIs of activity packages (e.g. HTTP, Email, Messaging). Ensure a versioned contract for activities. - - Use a shared test suite that activity package authors can run against their implementations. -- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. +- **Component tests**: Larger workflows with multiple activities, versioning, and persistence. Use real DB (Testcontainers or local ephemeral DB). Run in CI and locally. + - Use `TestHostFactory` with real DB provider. Import/publish workflow definitions from a `Workflows/` folder located in the root of the test (see `tests/component/*.ComponentTests/Scenarios/*` for more examples). Assert via DB queries or journal parsing. --- diff --git a/doc/qa/test-guidelines-integrators.md b/doc/qa/test-guidelines-integrators.md new file mode 100644 index 000000000..a3aa259aa --- /dev/null +++ b/doc/qa/test-guidelines-integrators.md @@ -0,0 +1,4 @@ + + +- **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. +- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. \ No newline at end of file From ca8c26c20b32265d2ecc05a49c89e1b0d4c5a1a9 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Wed, 1 Oct 2025 15:45:11 +0200 Subject: [PATCH 15/40] Expanding docs, better structure and goals --- doc/qa/test-guidelines-collaborators.md | 67 +++++++++++++++++++------ 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 333081c98..5c7d2e570 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -7,6 +7,16 @@ Additionally, it provides a consistent and resilient set of places to assert beh --- +## Summary +The philosophy of elsa test strategy can be summarized as: + +***Whenever a test fails, it should provide a clear direction towards the cause of the problem.*** +-- + +This means that if it does not point directly to the source of the issue, it should take the fewest possible amount of steps to get there. + +--- + ## Goals / Non‑functional requirements 1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. @@ -22,18 +32,56 @@ Additionally, it provides a consistent and resilient set of places to assert beh - **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - Use **xUnit** with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. -- **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake clock and fake scheduler. Run in CI and locally. - - Use `TestHostFactory` to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). +- **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake scheduler. Run in CI and locally. + - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. + - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). - **Component tests**: Larger workflows with multiple activities, versioning, and persistence. Use real DB (Testcontainers or local ephemeral DB). Run in CI and locally. - - Use `TestHostFactory` with real DB provider. Import/publish workflow definitions from a `Workflows/` folder located in the root of the test (see `tests/component/*.ComponentTests/Scenarios/*` for more examples). Assert via DB queries or journal parsing. + - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. Place workflow definitions in a `Workflows/` folder located in the root of the test (see `[ExecuteWorkflowsTests]` (../../tests/component/Elsa.Workflows.ComponentTests/Scenarios/) for more examples). Assert via DB queries or journal parsing. --- +## Elsa aspects to be tested: +- **Activities**: + - Unit test each activity class in isolation. + - all configurations and edge cases. + - Integration test activities using [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) for less common, tricky scenarios (see [`ForEachTests`](../../test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs) as an example). +- **Workflow execution**: + - Test workflow lifecycle, input/output, bookmarks, persistence, and resumption. + - Use `RunWorkflowUntilEndAsync` extension method in [`RunWorkflowExtensions`](../../src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs) for deterministic execution. +- **Persistence**: + - Test each [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) implementation. + - Test different storages of variables (Workflow Instance, Memory). +- **Serialization**: + - Unit test JSON serialization and deserialization of workflow definitions and instances. + - Integration test roundtrip of definitions through the publish/import API. +- **Triggers:** + - Test triggers for correct scheduling, invocation and resuming of workflows. +- **API**: + - Test HTTP endpoints for workflow execution, definition management, and instance querying. + + +## Unit tests + +Do when: +- Testing individual activity logic or small components in isolation. +- No need for persistence or real workflow execution. + +## Integration tests + +Do when: +- Testing workflow execution with multiple activities. +- Need to validate persistence, bookmarks, and resumption. +- Testing core engine components (WorkflowInvoker, Bookmark handling). + +## Component tests + +Do when: + ## Key constraints & recommended patterns ### 1. Should not be affected by execution times / Not depend on delays unless there is no other way -- **Synchronous execution mode**: Provide a test helper that runs workflows synchronously to completion when possible (e.g. `TestWorkflowRunner.RunToCompletion(workflowDefinition, inputs)`) — this executes activities inline and returns once the workflow is idle or complete. Use this for most assertions instead of waiting for background workers. +- **Synchronous execution mode**: Use this for most assertions instead of waiting for background workers. - **Avoid arbitrary sleeps**: If polling is necessary (e.g. for external system integration), use exponential backoff with short upper bounds and strong invariants (correlation ids) to detect test success quickly. @@ -102,7 +150,7 @@ Two main patterns: - Good for tests that validate runtime behavior without involving persistence or designer serialization. **B. Serialized definitions (recommended for integration & E2E tests)** -- Store JSON/YAML workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. +- Store JSON workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. - Tests are responsible for *publishing* the definitions into the test host if the scenario requires persistence (e.g. testing versioned definitions or import behavior). - For bulk tests, provide an import script (`ImportTestDefinitions.sh`) that uploads all artifacts in a single batch before running assertions. @@ -110,7 +158,6 @@ Two main patterns: - Component tests: code-first programmatic definitions. - Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. - ### 3. Bulk import - Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: @@ -119,7 +166,6 @@ Two main patterns: - Run in parallel but enforce deterministic ordering when versions matter. - For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. - ### 4. Working with Docker, env, K8s cluster deployments - **Test strategy split**: @@ -131,14 +177,12 @@ Two main patterns: - **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. - ### 5. Managing test and workflow definition versions - **Source control the workflow artifacts** and apply semantic versioning to their filenames or metadata (e.g. `payment-process.v1.2.0.json`). - **Immutable artifacts**: Do not overwrite a published artifact used by tests. If a workflow changes, publish a new version and update tests to point to the new artifact. - **Automate snapshots**: On CI, capture the actual deployed workflow definition version (ID + version) and record it with test results for traceability. - ### 6. Avoiding repetition **Test lifecycle template (common for integration/E2E tests)** @@ -160,7 +204,6 @@ Two main patterns: Include these helpers in a shared test utilities NuGet/package so all test projects can reuse them and reduce duplication. - ### 7. Assertion targets and alternatives #### Journal / Activity Execution Endpoints @@ -182,7 +225,6 @@ Include these helpers in a shared test utilities NuGet/package so all test proje - For unit and component tests, assert on in‑process events and test probes. For integration/E2E tests assert on durable state in the DB and validated events (or journal), and use correlation ids to make queries deterministic. Avoid relying solely on formatted journal lines. - Always propagate and assert on correlation IDs attached to workflow instances and events to locate the exact instance you need. - ### 8. Execute endpoint vs HTTP activity for tests - **Testing activities in isolation**: Unit test each activity class by constructing an `ActivityContext` and invoking `ExecuteAsync()` or using an `ActivityTestProbe`. This is the fastest and most isolated option. @@ -195,7 +237,6 @@ Include these helpers in a shared test utilities NuGet/package so all test proje **Recommendation:** Unit test activities directly. Integration test the activity inside workflows with the in‑process invoker. Reserve HTTP‑based tests for integrations and full server behavior validation. - ### 9. How to test failures (fail activity, missing instance, etc.) - **Explicit fail activities**: Unit test fail activities and assert they raise the expected error code and cause the workflow instance to transition to the appropriate state (e.g. Faulted). In integration tests, run the workflow to the failure point synchronously where possible and assert instance state. @@ -208,14 +249,12 @@ Include these helpers in a shared test utilities NuGet/package so all test proje - **Simulating host failures**: In integration tests, simulate crashes by killing the host process/container mid‑execution and restarting it to validate persistence and resume semantics. Use persistent DB so state survives host restart. - ### 10. Avoid instance ambiguity - **Tag instances on creation**: Allow tests to send an explicit `TestCorrelationId` or `TestTag` as part of workflow input/metadata. Persist this tag to the instance record. Use it to query the DB deterministically. - **Return the instance id on start**: Ensure test harness captures the created instance id from the start API or in‑process invoker and uses that id for all subsequent queries. - ### 11. Consistent execution environment - **Deterministic defaults**: For tests, use known configuration values (e.g. `MaxRetries=0`, `ShortCircuitLongRunning=true`) to eliminate production variability. From fba5f2a5a57e93d7f485f8f638a1b25c8e81dcea Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 2 Oct 2025 11:50:13 +0200 Subject: [PATCH 16/40] Removing irrelevant info of collaborators guideline, improving sections --- doc/qa/test-guidelines-collaborators.md | 192 +++++++----------------- doc/qa/test-guidelines-integrators.md | 46 +++++- 2 files changed, 98 insertions(+), 140 deletions(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 5c7d2e570..0a89e95c9 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -33,10 +33,14 @@ This means that if it does not point directly to the source of the issue, it sho - **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - Use **xUnit** with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. - **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake scheduler. Run in CI and locally. - - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. Use code-first or serialized workflow definitions depending on the amount and scope. + - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. + - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs) for an example). + - Use serialized workflow or code-first definitions depending on the amount and scope. - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). - **Component tests**: Larger workflows with multiple activities, versioning, and persistence. Use real DB (Testcontainers or local ephemeral DB). Run in CI and locally. - - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. Place workflow definitions in a `Workflows/` folder located in the root of the test (see `[ExecuteWorkflowsTests]` (../../tests/component/Elsa.Workflows.ComponentTests/Scenarios/) for more examples). Assert via DB queries or journal parsing. + - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. + - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs) for an example). + - Assert via DB queries or journal parsing. --- @@ -53,7 +57,7 @@ This means that if it does not point directly to the source of the issue, it sho - Test different storages of variables (Workflow Instance, Memory). - **Serialization**: - Unit test JSON serialization and deserialization of workflow definitions and instances. - - Integration test roundtrip of definitions through the publish/import API. + - Integration test roundtrip of definitions through API. - **Triggers:** - Test triggers for correct scheduling, invocation and resuming of workflows. - **API**: @@ -73,6 +77,13 @@ Do when: - Need to validate persistence, bookmarks, and resumption. - Testing core engine components (WorkflowInvoker, Bookmark handling). +For component tests, we don't have to register code-first workflows, but we do need to do so explicitly for integration tests when creating the TestApplicationBuilder. For example: +```csharp +_services = new TestApplicationBuilder(testOutputHelper) + .WithWorkflowsFromDirectory("Scenarios", "DependencyWorkflows", "Workflows") + .Build(); +``` + ## Component tests Do when: @@ -95,87 +106,60 @@ Do when: ### Example: Event-driven completion helper -Tests can subscribe to Elsa's event bus and await a specific event. +Tests can run workflows using the `RunWorkflowUntilEndAsync` extension method in [`RunWorkflowExtensions`](../../src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs) to reliably await for the execution without using `Thread.Sleep` or `Task.Delay`. -```csharp -public static class WorkflowEventWaiter -{ - public static Task WaitForWorkflowCompletedAsync( - IEventPublisher publisher, - string instanceId, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - timeout ??= TimeSpan.FromSeconds(30); - - void Handler(WorkflowCompleted evt) - { - if (evt.WorkflowInstanceId == instanceId) - tcs.TrySetResult(evt); - } - - publisher.Subscribe(Handler); - - _ = Task.Delay(timeout.Value, cancellationToken) - .ContinueWith(_ => tcs.TrySetException(new TimeoutException($\"Workflow {instanceId} did not complete within {timeout}.\"))); - - return tcs.Task; - } -} -``` Usage in a test: ```csharp -// Start workflow via /execute -var instanceId = response.InstanceId; +private readonly IServiceProvider _services; -// Wait for the event -var completedEvent = await WorkflowEventWaiter.WaitForWorkflowCompletedAsync(publisher, instanceId); -Assert.Equal(instanceId, completedEvent.WorkflowInstanceId); +public Tests(ITestOutputHelper testOutputHelper) +{ + _services = new TestApplicationBuilder(testOutputHelper) + .Build(); +} + +[Fact] +public async Task Test1() +{ + // Populate registries + await _services.PopulateRegistriesAsync(); + + // Import workflows + await _services.ImportWorkflowDefinitionAsync("Workflows/workflow-1.json"); + await _services.ImportWorkflowDefinitionAsync("Workflows/workflow-2.json"); + + // Run + var workflowState = await _services.RunWorkflowUntilEndAsync("my-workflow"); + + // Assert + // ....... +} ``` -This event-driven strategy ensures assertions only happen once Elsa signals the workflow is complete, making tests both fast and deterministic. +This extension method ensures assertions only happen once the workflow is complete, making tests both fast and deterministic. --- -### 2. Importing and publishing workflow definitions for testing workflows +### Importing workflow definitions for testing workflows Two main patterns: -**A. Programmatic/Code‑first registration (recommended for component tests)** -- Register workflows as code (C# fluent `IWorkflowBuilder`) inside test setup. This is fast and avoids serialization roundtrips. -- Use a lightweight in‑memory `IWorkflowRegistry` implementation for tests. +**A. Code‑first registration (recommended for component tests)** +- Register workflows as code inside test setup. This is fast and avoids serialization roundtrips. - Good for tests that validate runtime behavior without involving persistence or designer serialization. +- Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs) for an example). -**B. Serialized definitions (recommended for integration & E2E tests)** -- Store JSON workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. -- Tests are responsible for *publishing* the definitions into the test host if the scenario requires persistence (e.g. testing versioned definitions or import behavior). -- For bulk tests, provide an import script (`ImportTestDefinitions.sh`) that uploads all artifacts in a single batch before running assertions. +**B. Serialized definitions (recommended for integration tests)** +- Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs) for an example). **Which to use?** -- Component tests: code-first programmatic definitions. -- Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. +- Integration tests: use serialized JSON workflows. +- Component tests: code-first definitions or workflows created via designer and exported to JSON to validate persistence, designer output and versioning behavior. +--- -### 3. Bulk import -- Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: - - Validate schema and version. - - Report conflicts or duplicate IDs. - - Run in parallel but enforce deterministic ordering when versions matter. -- For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. - -### 4. Working with Docker, env, K8s cluster deployments - -- **Test strategy split**: - - Local developer tests: use in‑process hosts and in‑memory/ephemeral DBs (SQLite in-memory or Testcontainers-based DB). Fast and deterministic. - - CI Docker Compose: spin up a lightweight containerized environment with the host app, a real DB (Postgres, SQL Server or Mongo) and optional message broker; use Testcontainers (or Docker Compose) to orchestrate in CI. - - K8s E2E: run a small suite that deploys a test namespace with Helm or apply manifests. Use ephemeral resources and ensure cleanup. Keep these tests in a separate CI stage. - -- **Use Testcontainers** (or equivalent) to provision ephemeral DBs/brokers in CI; this keeps environments close to production while still being isolated and reproducible. - -- **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. ### 5. Managing test and workflow definition versions @@ -185,7 +169,7 @@ Two main patterns: ### 6. Avoiding repetition -**Test lifecycle template (common for integration/E2E tests)** +**Test lifecycle template (common for integration tests)** 1. **Provision** the environment (in‑process host or containerized test environment). 2. **Reset** persistence (drop / recreate DB schema or use a clean DB instance). 3. **Import/Publish** required workflow definitions. @@ -263,77 +247,7 @@ Include these helpers in a shared test utilities NuGet/package so all test proje --- -## Concrete examples & small templates -> The repository should provide a `tests/test-utilities` project with the following helpers that are *reusable by all test projects*. - -### `TestHostFactory` (conceptual) - -```csharp -public static IHost CreateTestHost(Action configure) -{ - var host = Host.CreateDefaultBuilder() - .ConfigureServices((ctx, services) => - { - services.AddElsa(elsa => - { - // Use in-memory persistence and deterministic scheduler for tests - elsa.UseInMemoryPersistence(); - elsa.UseDeterministicScheduler(); - }); - - // Additional test overrides - configure?.Invoke(services); - }) - .Build(); - - host.Start(); - return host; -} -``` - -### `DeterministicScheduler` interface - -```csharp -public interface IDeterministicScheduler -{ - Task> GetDueBookmarksAsync(); - Task TriggerBookmarkAsync(string bookmarkId, string correlationId = null); -} -``` - -### Example test flow (integration, in‑process) - -1. Create host with `TestHostFactory`. -2. Load workflow definitions (code‑first or JSON loader). -3. Start workflow via `IWorkflowInvoker.StartAsync(definitionId, inputs, correlationId)` -> returns `instanceId`. -4. If workflow waits on bookmark, call `scheduler.TriggerBookmarkAsync(bookmarkId, correlationId)`. -5. Assert final instance status via `IWorkflowInstanceStore.FindById(instanceId)` or via `IEventCollector`. - - -### Example manifest (tests/manifests/decision.json) - -```json -{ - "suiteName": "decision", - "definitions": [ - { "id": "decision-true", "version": "1.2.0", "path": "definitions/decision-true.v1.2.0.json" }, - { "id": "decision-true", "version": "1.0.0", "path": "definitions/decision-false.v1.0.0.json" } - ] -} -``` - ---- - -## CI recommendations - -- **Local unit tests**: run in `dotnet test` step (fast). Use in‑memory stores and determinism. -- **Integration tests**: run with Testcontainers (or similar) to provide real DB/broker. Run these in a separate CI job because they are slower. -- **K8s smoke tests**: optional separate stage. Deploy to ephemeral namespace via Helm and run a small suite of smoke E2E tests; tear down after. -- **Parallelization**: run multiple test matrices (DB providers) but avoid running heavy E2E jobs in parallel unless you have isolated resources. -- **Flaky test detection**: enable a flaky test retry policy for known non‑deterministic tests, but treat retries as signals to fix the underlying determinism problems. - ---- ## Failure injection and resilience testing @@ -360,10 +274,10 @@ public interface IDeterministicScheduler 1. **Increase** unit test coverage of existing code using these patterns. 2. **Add `IDeterministicScheduler` abstractions** and implement test doubles. -2. **Create `tests/test-utilities` project** and convert a couple of existing tests to use it as examples. -3. **Define manifest schema** and commit a couple of versioned workflow definitions to `tests/definitions/`. -4. **Add one CI integration job** that uses Testcontainers (or similar) to run the integration suite against popular db providers (MySql, Postgres and Mongo, for example). -5. **Add telemetry and event collectors** to support in‑process assertions (easier than parsing journal text). +3. **Create `tests/test-utilities` project** and convert a couple of existing tests to use it as examples. +4. **Define manifest schema** and commit a couple of versioned workflow definitions to `tests/definitions/`. +5. **Add one CI integration job** that uses Testcontainers (or similar) to run the integration suite against popular db providers (MySql, Postgres and Mongo, for example). +6. **Add telemetry and event collectors** to support in‑process assertions (easier than parsing journal text). --- ## Appendix: Quick checklist for writing a new test diff --git a/doc/qa/test-guidelines-integrators.md b/doc/qa/test-guidelines-integrators.md index a3aa259aa..a3773ba7f 100644 --- a/doc/qa/test-guidelines-integrators.md +++ b/doc/qa/test-guidelines-integrators.md @@ -1,4 +1,48 @@ - **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. -- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. \ No newline at end of file +- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. + +### Importing and publishing workflow definitions for testing workflows + +Two main patterns: + +**A. Code‑first registration (recommended for component tests)** +- Register workflows as code inside test setup. This is fast and avoids serialization roundtrips. +- Good for tests that validate runtime behavior without involving persistence or designer serialization. + +**B. Serialized definitions (recommended for integration & E2E tests)** +- Store JSON workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. +- Tests are responsible for *publishing* the definitions into the test host if the scenario requires persistence (e.g. testing versioned definitions or import behavior). + +**Which to use?** +- Component tests: code-first definitions or workflows created via designer and exported to JSON. +- Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. + +### Bulk import + +- Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: + - Validate schema and version. + - Report conflicts or duplicate IDs. + - Run in parallel but enforce deterministic ordering when versions matter. +- For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. + + +### Working with Docker, env, K8s cluster deployments + +- **Test strategy split**: + - Local developer tests: use in‑process hosts and in‑memory/ephemeral DBs (SQLite in-memory or Testcontainers-based DB). Fast and deterministic. + - CI Docker Compose: spin up a lightweight containerized environment with the host app, a real DB (Postgres, SQL Server or Mongo) and optional message broker; use Testcontainers (or Docker Compose) to orchestrate in CI. + - K8s E2E: run a small suite that deploys a test namespace with Helm or apply manifests. Use ephemeral resources and ensure cleanup. Keep these tests in a separate CI stage. + +- **Use Testcontainers** (or equivalent) to provision ephemeral DBs/brokers in CI; this keeps environments close to production while still being isolated and reproducible. + +- **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. + +### CI recommendations + +- **Local unit tests**: run in `dotnet test` step (fast). Use in‑memory stores and determinism. +- **Integration tests**: run with Testcontainers (or similar) to provide real DB/broker. Run these in a separate CI job because they are slower. +- **K8s smoke tests**: optional separate stage. Deploy to ephemeral namespace via Helm and run a small suite of smoke E2E tests; tear down after. +- **Parallelization**: run multiple test matrices (DB providers) but avoid running heavy E2E jobs in parallel unless you have isolated resources. +- **Flaky test detection**: enable a flaky test retry policy for known non‑deterministic tests, but treat retries as signals to fix the underlying determinism problems. From 407868b9a13f17c65ca12f4a6a7b96c2e08b7539 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 2 Oct 2025 12:08:44 +0200 Subject: [PATCH 17/40] Improving readability --- doc/qa/test-guidelines-collaborators.md | 36 ++++++++++++++++--------- doc/qa/test-guidelines-integrators.md | 11 ++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 0a89e95c9..97e9f8e19 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -1,8 +1,11 @@ # Elsa Core — Test Strategy Guidelines for Collaborators **Purpose:** + This document describes recommended testing strategies for the Elsa engine. -It is written for collaborators and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient across local, Docker and Kubernetes CI environments. + +It is written for collaborators and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. + Additionally, it provides a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. --- @@ -22,24 +25,27 @@ This means that if it does not point directly to the source of the issue, it sho 1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. 2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. 3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. -4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. -5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. -6. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. +4. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. +5. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. --- ## High‑level testing pyramid for Elsa -- **Unit tests**: Activity logic, expression evaluators, small helpers. In‑process, mocking storage and scheduler. Fast and numerous. - - Use **xUnit** with [Moq / NSubstitute] for mocking. Test activities and small components in isolation. Use in‑memory stores. +- **Unit tests**: Activity logic, expression evaluators, small helpers, services and providers. In‑process, mocking storage and scheduler. Fast and numerous. + - Use **xUnit** with [Moq / NSubstitute] for mocking. + - Test activities and small components in isolation. + - Use in‑memory stores. - **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake scheduler. Run in CI and locally. - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. - - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs) for an example). + - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs)). - Use serialized workflow or code-first definitions depending on the amount and scope. - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). -- **Component tests**: Larger workflows with multiple activities, versioning, and persistence. Use real DB (Testcontainers or local ephemeral DB). Run in CI and locally. +- **Component tests**: Larger workflows with multiple activities, versioning, and persistence. + - Use real DB (local ephemeral DB). Run in CI and locally. + - Use real components (e.g. [`IWorkflowRuntime`](../../src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs), see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. - - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs) for an example). + - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Assert via DB queries or journal parsing. --- @@ -48,7 +54,7 @@ This means that if it does not point directly to the source of the issue, it sho - **Activities**: - Unit test each activity class in isolation. - all configurations and edge cases. - - Integration test activities using [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) for less common, tricky scenarios (see [`ForEachTests`](../../test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs) as an example). + - Integration test activities using [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) for less common, tricky scenarios (e.g. [`ForEachTests`](../../test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs)). - **Workflow execution**: - Test workflow lifecycle, input/output, bookmarks, persistence, and resumption. - Use `RunWorkflowUntilEndAsync` extension method in [`RunWorkflowExtensions`](../../src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs) for deterministic execution. @@ -66,9 +72,13 @@ This means that if it does not point directly to the source of the issue, it sho ## Unit tests -Do when: -- Testing individual activity logic or small components in isolation. -- No need for persistence or real workflow execution. +Do when testing: +- Individual activity logic. +- Small components in isolation. +- Logic before/after persistence . +- Logic before/after interactions between components. +- Expression evaluation. +- Providers and services (elsa implementations). ## Integration tests diff --git a/doc/qa/test-guidelines-integrators.md b/doc/qa/test-guidelines-integrators.md index a3773ba7f..8eddc7c25 100644 --- a/doc/qa/test-guidelines-integrators.md +++ b/doc/qa/test-guidelines-integrators.md @@ -1,4 +1,15 @@ +## Goals / Non‑functional requirements + +1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. +2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. +3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. +4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. +5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. +6. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. + +--- + - **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. - **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. From 24390e0adc17f8e406be1bbc2184e376b98fb2ef Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 09:56:24 +0200 Subject: [PATCH 18/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 97e9f8e19..b0e44509a 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -1,4 +1,4 @@ -# Elsa Core — Test Strategy Guidelines for Collaborators +# Elsa Core — Test Strategy Guidelines for Contributors **Purpose:** From 62c7ed44bc48f4335594b86b2669202d5d7d40a1 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 09:56:34 +0200 Subject: [PATCH 19/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index b0e44509a..164c442cd 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -2,7 +2,7 @@ **Purpose:** -This document describes recommended testing strategies for the Elsa engine. +This document describes recommended testing strategies when contributing features and fixes to the Elsa Workflows project. It is written for collaborators and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. From e6c8e9835fd591d985f5a11a8b571fe3ea1e1f91 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 09:56:43 +0200 Subject: [PATCH 20/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 164c442cd..3950aef7e 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -4,7 +4,7 @@ This document describes recommended testing strategies when contributing features and fixes to the Elsa Workflows project. -It is written for collaborators and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. +It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. Additionally, it provides a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. From 544f9d5221d352eb4bfa637c8dc9db2b6b4a5e3e Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 09:56:52 +0200 Subject: [PATCH 21/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 3950aef7e..b3a4a4595 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -11,7 +11,7 @@ Additionally, it provides a consistent and resilient set of places to assert beh --- ## Summary -The philosophy of elsa test strategy can be summarized as: +The philosophy of the Elsa test strategy can be summarized as: ***Whenever a test fails, it should provide a clear direction towards the cause of the problem.*** -- From e273239fd051478d82d78ea23c4d0fb1a40f1ab0 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 09:57:03 +0200 Subject: [PATCH 22/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index b3a4a4595..dfa49dffa 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -22,7 +22,7 @@ This means that if it does not point directly to the source of the issue, it sho ## Goals / Non‑functional requirements -1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. +1. **Deterministic tests** — tests should not be flaky, meaning they should produce the same results consistently on subsequent runs. 2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. 3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. 4. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. From 93676595d4574953d4bdad716dad2288aa9732ce Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:42:14 +0200 Subject: [PATCH 23/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index dfa49dffa..20c88d0fa 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -6,7 +6,7 @@ This document describes recommended testing strategies when contributing feature It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. -Additionally, it provides a consistent and resilient set of places to assert behavior (journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. +Additionally, it provides a consistent and resilient set of places to assert behavior (e.g., journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. --- From 7365eb575d3526b342baf7c904553d3af1cbd070 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:42:36 +0200 Subject: [PATCH 24/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 20c88d0fa..c31effe1a 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -36,7 +36,7 @@ This means that if it does not point directly to the source of the issue, it sho - Use **xUnit** with [Moq / NSubstitute] for mocking. - Test activities and small components in isolation. - Use in‑memory stores. -- **Integration tests**: Core engine components (WorkflowInvoker, Bookmark handling, persistence adapters) with in‑memory or ephemeral DB. Use fake scheduler. Run in CI and locally. +- **Integration tests**: Core engine components (e.g., workflow invocation, bookmark handling, persistence adapters) with in‑memory or ephemeral databases. Use fake schedulers. Run in CI and locally. - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs)). - Use serialized workflow or code-first definitions depending on the amount and scope. From e0f353fb3508407d43817e52cac7399088a97402 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:43:57 +0200 Subject: [PATCH 25/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index c31effe1a..4ec82c271 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -41,7 +41,7 @@ This means that if it does not point directly to the source of the issue, it sho - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs)). - Use serialized workflow or code-first definitions depending on the amount and scope. - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). -- **Component tests**: Larger workflows with multiple activities, versioning, and persistence. +- **Component tests**: Exercises multiple components end to end, e.g. all the way from a REST API endpoint down to the persistence layer, touching everything in between. - Use real DB (local ephemeral DB). Run in CI and locally. - Use real components (e.g. [`IWorkflowRuntime`](../../src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs), see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. From 2f16ad28d3f8af3503b08f64dd2eed3f7029c4b1 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:44:18 +0200 Subject: [PATCH 26/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 4ec82c271..3e8cebf30 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -42,7 +42,7 @@ This means that if it does not point directly to the source of the issue, it sho - Use serialized workflow or code-first definitions depending on the amount and scope. - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). - **Component tests**: Exercises multiple components end to end, e.g. all the way from a REST API endpoint down to the persistence layer, touching everything in between. - - Use real DB (local ephemeral DB). Run in CI and locally. + - Use real DB (local / containerized, ephemeral databases). Run in CI and locally. - Use real components (e.g. [`IWorkflowRuntime`](../../src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs), see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). From 900adc66fcdbfc81e3e735b50051b4b75e24ca93 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:44:41 +0200 Subject: [PATCH 27/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 3e8cebf30..13d3a0c05 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -44,7 +44,7 @@ This means that if it does not point directly to the source of the issue, it sho - **Component tests**: Exercises multiple components end to end, e.g. all the way from a REST API endpoint down to the persistence layer, touching everything in between. - Use real DB (local / containerized, ephemeral databases). Run in CI and locally. - Use real components (e.g. [`IWorkflowRuntime`](../../src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs), see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with real DB provider. + - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with a real persistence provider. - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Assert via DB queries or journal parsing. From 50ce9054a07e9e2b212f220ca83cbe764eb1e75f Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:45:07 +0200 Subject: [PATCH 28/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 13d3a0c05..278d209fa 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -47,6 +47,8 @@ This means that if it does not point directly to the source of the issue, it sho - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with a real persistence provider. - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - Assert via DB queries or journal parsing. + - Possible to install spies with DI and inspect them in the assertion steps. + --- From 071d8eb2a8ce7508fcde74a4acbdb5b308dddb47 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Fri, 3 Oct 2025 12:45:25 +0200 Subject: [PATCH 29/40] Update doc/qa/test-guidelines-collaborators.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines-collaborators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index 278d209fa..fac223da3 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -52,7 +52,7 @@ This means that if it does not point directly to the source of the issue, it sho --- -## Elsa aspects to be tested: +## Aspects to be tested: - **Activities**: - Unit test each activity class in isolation. - all configurations and edge cases. From 3d9cefa8940a9b55b79a0df92d162d3f95e85f8b Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 6 Oct 2025 17:54:11 +0200 Subject: [PATCH 30/40] Better and revised general testing guidelines --- Elsa.sln | 1 - doc/qa/test-guidelines-collaborators.md | 446 +++++++++++------------- doc/qa/test-guidelines-integrators.md | 59 ---- 3 files changed, 201 insertions(+), 305 deletions(-) delete mode 100644 doc/qa/test-guidelines-integrators.md diff --git a/Elsa.sln b/Elsa.sln index e6a087781..2dec82de1 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -301,7 +301,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-4667-ADC2-37C62C9C2574}" ProjectSection(SolutionItems) = preProject doc\qa\test-guidelines-collaborators.md = doc\qa\test-guidelines-collaborators.md - doc\qa\test-guidelines-integrators.md = doc\qa\test-guidelines-integrators.md EndProjectSection EndProject Global diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines-collaborators.md index fac223da3..618e39ee4 100644 --- a/doc/qa/test-guidelines-collaborators.md +++ b/doc/qa/test-guidelines-collaborators.md @@ -1,306 +1,262 @@ -# Elsa Core — Test Strategy Guidelines for Contributors +# Elsa Core — Testing Strategy -**Purpose:** +## Purpose -This document describes recommended testing strategies when contributing features and fixes to the Elsa Workflows project. - -It is written for contributors and aims to provide concrete, repeatable patterns you can adopt in unit, integration and component tests to make test execution deterministic, fast, and resilient. - -Additionally, it provides a consistent and resilient set of places to assert behavior (e.g., journal, activity execution endpoints, DB queries, events) and guidelines for choosing between them. +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 the Elsa test strategy can be summarized as: +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.*** --- -This means that if it does not point directly to the source of the issue, it should take the fewest possible amount of steps to get there. +Tests should be fast, deterministic, and precise: they should pinpoint the failing subsystem (activity, invoker, persistence, scheduler) with minimal noise. + +For contributors, tests are the first line of code review: they must document intended behaviour and prevent regressions. --- -## Goals / Non‑functional requirements +## High-level testing pyramid -1. **Deterministic tests** — tests should not be flaky, meaning they should produce the same results consistently on subsequent runs. -2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. -3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. -4. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. -5. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. +- **Unit tests** — single-class logic (activities, converters, expression evaluators, serializers, service providers). Fast; no persistence. +- **Integration tests** — multiple Elsa subsystems together (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. --- -## High‑level testing pyramid for Elsa +## Quick Start for Contributors -- **Unit tests**: Activity logic, expression evaluators, small helpers, services and providers. In‑process, mocking storage and scheduler. Fast and numerous. - - Use **xUnit** with [Moq / NSubstitute] for mocking. - - Test activities and small components in isolation. - - Use in‑memory stores. -- **Integration tests**: Core engine components (e.g., workflow invocation, bookmark handling, persistence adapters) with in‑memory or ephemeral databases. Use fake schedulers. Run in CI and locally. - - Use [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) to create test hosts with DI overrides. - - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs)). - - Use serialized workflow or code-first definitions depending on the amount and scope. - - Also, possible to use external tooling like [JTest](https://github.com/nexxbiz/jtest). -- **Component tests**: Exercises multiple components end to end, e.g. all the way from a REST API endpoint down to the persistence layer, touching everything in between. - - Use real DB (local / containerized, ephemeral databases). Run in CI and locally. - - Use real components (e.g. [`IWorkflowRuntime`](../../src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs), see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - - Use [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) with a real persistence provider. - - Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs)). - - Assert via DB queries or journal parsing. - - Possible to install spies with DI and inspect them in the assertion steps. - +**Before you write 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? → 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`) --- -## Aspects to be tested: -- **Activities**: - - Unit test each activity class in isolation. - - all configurations and edge cases. - - Integration test activities using [`TestApplicationBuilder`](../../src/common/Elsa.Testing.Shared.Integration/TestApplicationBuilder.cs) for less common, tricky scenarios (e.g. [`ForEachTests`](../../test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs)). -- **Workflow execution**: - - Test workflow lifecycle, input/output, bookmarks, persistence, and resumption. - - Use `RunWorkflowUntilEndAsync` extension method in [`RunWorkflowExtensions`](../../src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs) for deterministic execution. -- **Persistence**: - - Test each [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) implementation. - - Test different storages of variables (Workflow Instance, Memory). -- **Serialization**: - - Unit test JSON serialization and deserialization of workflow definitions and instances. - - Integration test roundtrip of definitions through API. -- **Triggers:** - - Test triggers for correct scheduling, invocation and resuming of workflows. -- **API**: - - Test HTTP endpoints for workflow execution, definition management, and instance querying. +## 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. -## Unit tests +- **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. -Do when testing: -- Individual activity logic. -- Small components in isolation. -- Logic before/after persistence . -- Logic before/after interactions between components. -- Expression evaluation. -- Providers and services (elsa implementations). +--- -## Integration tests +## Which parts of Elsa to test, and which test types to use -Do when: -- Testing workflow execution with multiple activities. -- Need to validate persistence, bookmarks, and resumption. -- Testing core engine components (WorkflowInvoker, Bookmark handling). +This section maps Elsa aspects to the exact kinds of tests you should write, with examples and code patterns referencing repository conventions. -For component tests, we don't have to register code-first workflows, but we do need to do so explicitly for integration tests when creating the TestApplicationBuilder. For example: +### Activities + +#### **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. + +**Example:** ```csharp -_services = new TestApplicationBuilder(testOutputHelper) - .WithWorkflowsFromDirectory("Scenarios", "DependencyWorkflows", "Workflows") - .Build(); +// Arrange +var serviceProvider = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(capturingTextWriter) + .Build(); + +// Act +var writeLine = new WriteLine("Hello world!"); +await serviceProvider.RunActivityAsync(writeLine); + +// Assert +Assert.Equal("Hello world!", capturingTextWriter.Lines.Single()); ``` -## Component tests +#### **Integration tests (recommended if activity participates in workflows):** +- 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. -Do when: +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. -## Key constraints & recommended patterns -### 1. Should not be affected by execution times / Not depend on delays unless there is no other way +### Workflow execution (invoker, middleware, bookmarks) -- **Synchronous execution mode**: Use this for most assertions instead of waiting for background workers. +#### Unit tests: +- Rare: low-level pure helpers in the invoker may have unit tests for edge cases. Most invoker behavior requires integration testing. -- **Avoid arbitrary sleeps**: If polling is necessary (e.g. for external system integration), use exponential backoff with short upper bounds and strong invariants (correlation ids) to detect test success quickly. +#### 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. -- **Prefer manual triggers**: For timers or external events, design tests to call the engine's trigger API (e.g. raise signal, post message, call `ResumeBookmark`) rather than waiting for a timer to fire. -- **Async completion waiters**: When invoking workflows through the HTTP `/execute` endpoint (which returns immediately), provide a helper, such as `WaitForCompletionAsync(instanceId)` (example below) that subscribes to completion events. This replaces fragile `Thread.Sleep` patterns and ensures tests only assert once the workflow has actually finished. +Call the workflow runner to execute a workflow object or a loaded definition. Prefer this when asserting logical flow and outputs. -- **Event-driven assertions**: Subscribe to engine events (WorkflowCompleted, ActivityExecuted, etc.) in tests. Block until the event for the specific instance arrives instead of waiting arbitrary amounts of time. +```csharp +var runner = serviceProvider.GetRequiredService(); +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); +``` + +### Persistence & Serialization + +#### Integration tests:** +- Import a JSON workflow definition via the same serializers used by the engine (the test helper [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) demonstrates this pattern). Run the workflow through [`IWorkflowRunner`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) to validate deserialization + execution. +--- + +## 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 tests | +| `PopulateRegistriesAsync` | Register types for JSON deserialization | Loading JSON workflows | +| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) | +| `RunWorkflowUntilEndAsync` | Drive workflow to completion | Complex resumption scenarios | --- -### Example: Event-driven completion helper +## Decision helper (what to add — follow in order) -Tests can run workflows using the `RunWorkflowUntilEndAsync` extension method in [`RunWorkflowExtensions`](../../src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs) to reliably await for the execution without using `Thread.Sleep` or `Task.Delay`. +1. **Changed code is a single activity class with no persistence/external calls?** → Unit test only. +2. **Change touches invoker/scheduler/bookmarks or workflow composition?** → Integration test using [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and a small workflow. If persistence semantics change, add component tests. +3. **Change touches persistence/serializers or requires durable evidence (journal, bookmarks)?** → Component tests against [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). +When in doubt, add the minimal unit tests plus one integration test that reproduces the scenario. -Usage in a test: +--- + +## 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. +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). + +--- + +## Failure testing (faults & incidents) + +- **Unit test [`Fault`](../../src/modules/Elsa.Workflows.Core/Activities/Fault.cs) activity**: instantiate the [`Fault`](../../src/modules/Elsa.Workflows.Core/Activities/Fault.cs) activity class and assert the expected exception/behavior. +- **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. + +**Tip:** tests that simulate host restart should recreate the service provider but reuse the same persistence store instance (in-memory DB configured at the test scope or repo test fixtures). This proves the engine resumes from persisted state. + +--- + +## Practical test recipes & snippets (copy/paste-ready) + +### Unit test (activity) — pattern ```csharp -private readonly IServiceProvider _services; - -public Tests(ITestOutputHelper testOutputHelper) -{ - _services = new TestApplicationBuilder(testOutputHelper) - .Build(); -} - [Fact] -public async Task Test1() +public async Task MyActivity_WritesExpectedOutput() { - // Populate registries - await _services.PopulateRegistriesAsync(); + var sp = new TestApplicationBuilder(testOutput).Build(); + var activity = new MyActivity { Input = "x" }; - // Import workflows - await _services.ImportWorkflowDefinitionAsync("Workflows/workflow-1.json"); - await _services.ImportWorkflowDefinitionAsync("Workflows/workflow-2.json"); + await sp.RunActivityAsync(activity); - // Run - var workflowState = await _services.RunWorkflowUntilEndAsync("my-workflow"); - - // Assert - // ....... + // assert behavior of activity in isolation } ``` -This extension method ensures assertions only happen once the workflow is complete, making tests both fast and deterministic. +### 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(); + var workflow = new MyWorkflowDefinition(); + + var result = await runner.RunAsync(workflow); + + Assert.Equal(WorkflowStatus.Finished, result.WorkflowInstance!.Status); +} +``` + +### Component test — pattern asserting persisted state + +```csharp +[Fact] +public async Task Workflow_Persists_Instance_And_Journal() +{ + var sp = new TestApplicationBuilder(testOutput) + .UseRealPersistenceForTests() + .Build(); + + var runner = sp.GetRequiredService(); + var store = sp.GetRequiredService(); + + var result = await runner.RunAsync(workflow); + var instanceId = result.WorkflowInstance!.Id; + + // deterministic lookup: query store until terminal state + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10); + WorkflowInstance? instance = null; + while (DateTime.UtcNow < deadline) + { + instance = await store.FindByIdAsync(instanceId); + if (instance is not null && instance.Status is WorkflowStatus.Finished or WorkflowStatus.Faulted) + break; + await Task.Delay(150); + } + + instance.Should().NotBeNull(); + instance!.Status.Should().Be(WorkflowStatus.Finished); +} +``` +--- + +## FAQ (quick pointers) + +**Q: How do I import workflow definitions in tests?** +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. + +**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). + +**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: Not yet — the repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and integration helpers that cover most scenarios. If you find many duplicated poll loops, open an issue requesting a canonical `WaitForCompletion` helper in `test/shared`. --- -### Importing workflow definitions for testing workflows +## Appendix — examples in the repository (where to look) -Two main patterns: +Search the `test/` tree for examples that follow the above patterns: -**A. Code‑first registration (recommended for component tests)** -- Register workflows as code inside test setup. This is fast and avoids serialization roundtrips. -- Good for tests that validate runtime behavior without involving persistence or designer serialization. -- Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`ExecuteWorkflowsTests`](../../test/component/Elsa.Workflows.ComponentTests/Scenarios/ExecuteWorkflows/ExecuteWorkflowsTests.cs) for an example). - -**B. Serialized definitions (recommended for integration tests)** -- Place workflow definitions in a `Workflows/` folder located in the root of the test (see [`MigrationTests`](../../test/integration/Elsa.Alterations.IntegrationTests/MigrationTests.cs) for an example). - -**Which to use?** -- Integration tests: use serialized JSON workflows. -- Component tests: code-first definitions or workflows created via designer and exported to JSON to validate persistence, designer output and versioning behavior. ---- +- 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). -### 5. Managing test and workflow definition versions - -- **Source control the workflow artifacts** and apply semantic versioning to their filenames or metadata (e.g. `payment-process.v1.2.0.json`). -- **Immutable artifacts**: Do not overwrite a published artifact used by tests. If a workflow changes, publish a new version and update tests to point to the new artifact. -- **Automate snapshots**: On CI, capture the actual deployed workflow definition version (ID + version) and record it with test results for traceability. - -### 6. Avoiding repetition - -**Test lifecycle template (common for integration tests)** -1. **Provision** the environment (in‑process host or containerized test environment). -2. **Reset** persistence (drop / recreate DB schema or use a clean DB instance). -3. **Import/Publish** required workflow definitions. -4. **Register** test hooks (e.g. fake scheduler, activity test doubles, callback endpoints). -5. **Invoke** the workflow via the API, direct invoker or trigger. -6. **Trigger** bookmarks manually if needed. -7. **Assert** via journal/events/DB/state. -8. **Tear down** environment and collect artifacts (logs, DB snapshots) on failures. - -**Reusable code artifacts** -- `TestHostFactory`: create and configure test hosts (DI container, fake services). -- `WorkflowDefinitionLoader`: loads definitions from disk, validates versions, and publishes them. -- `DeterministicScheduler`: test scheduler with `TriggerOnce(correlationId)`. -- `ActivityTestProbe`: an in‑process activity wrapper that captures inputs/outputs and emits structured events to assert against. -- JSON manifest schema for suite imports. - -Include these helpers in a shared test utilities NuGet/package so all test projects can reuse them and reduce duplication. - -### 7. Assertion targets and alternatives - -#### Journal / Activity Execution Endpoints - -**Pros** -- Journal provides a chronological, human‑readable trace of what happened and is close to production observability. -- Activity execution endpoints (if available) allow real API surface testing and validate telemetry and audit paths. - -**Cons** -- Journal may be high volume and require parsing to find relevant entries; tests risk being brittle if journal format changes. -- Accessing activity endpoints over HTTP introduces network flakiness in E2E tests. - -#### Alternatives / Complementary options -- **Direct DB queries**: Query the workflow instance table, bookmarks, and activity logs. Stronger for deterministic assertions about state (e.g. `WorkflowInstance.Status == Completed`). -- **Event stream / notifications**: Subscribe to internal events (in tests) via the mediator or a test `INotificationHandler` to assert lifecycle events as they happen in real time. -- **Activity test probes**: Instrument activities in tests to emit structured markers (test hooks) that are easier to assert than raw journal text. - -**Recommendations:** -- For unit and component tests, assert on in‑process events and test probes. For integration/E2E tests assert on durable state in the DB and validated events (or journal), and use correlation ids to make queries deterministic. Avoid relying solely on formatted journal lines. -- Always propagate and assert on correlation IDs attached to workflow instances and events to locate the exact instance you need. - -### 8. Execute endpoint vs HTTP activity for tests - -- **Testing activities in isolation**: Unit test each activity class by constructing an `ActivityContext` and invoking `ExecuteAsync()` or using an `ActivityTestProbe`. This is the fastest and most isolated option. - -- **Testing activities in workflow**: Component tests should compose small workflows in code and run them through the `WorkflowInvoker` to validate end‑to‑end semantics (including variable passing, bookmarks, parallelism). Keep these tests in‑process to avoid network boundaries. - -- **Testing via HTTP**: Use HTTP endpoint activities for integration and true E2E testing of the server host, middleware and serialization. These tests are slower and belong in the E2E suite. There are two possibilities: - - Workflow with the desired activity and a connected entry HTTP endpoint activity; - - `/execute` endpoint that runs the workflow directly, in this case, the HTTP endpoint activity is not necessary. - -**Recommendation:** Unit test activities directly. Integration test the activity inside workflows with the in‑process invoker. Reserve HTTP‑based tests for integrations and full server behavior validation. - -### 9. How to test failures (fail activity, missing instance, etc.) - -- **Explicit fail activities**: Unit test fail activities and assert they raise the expected error code and cause the workflow instance to transition to the appropriate state (e.g. Faulted). In integration tests, run the workflow to the failure point synchronously where possible and assert instance state. - -- **No workflow instance returned / missing instance**: - - Use correlation IDs passed at invocation time. The engine should return an `instanceId` or `correlationId` when starting. Tests should persist that id and query the instance store using it rather than using `GetLatest` semantics. - - If there are no guaranteed instance returns, wrap invocation in a test helper that extracts and returns the created instance id from either the API response, journal event, or DB entry. - -- **Correlated queries vs GetLatest**: Avoid `GetLatest` in tests because it is non‑deterministic in parallel runs. Use correlation ids, explicit instance IDs, or filters (workflow definition id + start time + unique test tag) to locate the exact instance. - -- **Simulating host failures**: In integration tests, simulate crashes by killing the host process/container mid‑execution and restarting it to validate persistence and resume semantics. Use persistent DB so state survives host restart. - -### 10. Avoid instance ambiguity - -- **Tag instances on creation**: Allow tests to send an explicit `TestCorrelationId` or `TestTag` as part of workflow input/metadata. Persist this tag to the instance record. Use it to query the DB deterministically. - -- **Return the instance id on start**: Ensure test harness captures the created instance id from the start API or in‑process invoker and uses that id for all subsequent queries. - -### 11. Consistent execution environment - -- **Deterministic defaults**: For tests, use known configuration values (e.g. `MaxRetries=0`, `ShortCircuitLongRunning=true`) to eliminate production variability. -- **Isolated DB per test process**: Use ephemeral DBs (unique DB name per test run) to avoid cross‑test contamination. -- **Artifact collection**: On failure, collect logs, DB snapshot and exported journal to help triage flakiness. - ---- - - - -## Failure injection and resilience testing - -- **Deterministic fault injection**: provide test stubs for activities that throw predictable exceptions on demand. Use configuration flags or special test input to trigger them. -- **Host process kill**: in containerized tests, kill the host midway (Docker/kill or stop container) and restart to verify persistence/resumption. -- **Network partitions**: simulate by blocking network connections to DB/broker in the test environment to ensure graceful failure handling. - ---- - -## What to deliver in the repository - -- `tests/test-utilities/` project with common helpers (TestHostFactory, DeterministicScheduler, Test probes) -- `tests/definitions/` with versioned workflow artifacts -- `tests/manifests/` for suites referencing definitions and versions -- `tests/ci/docker-compose.yml` and `tests/ci/k8s/` manifests for reproducible integration/E2E runs -- Example tests showing patterns: - - Unit test for `HttpRequestActivity` (activity isolation) - - Integration test for basic workflow (deterministic scheduler) - - E2E test that deploys a host in Docker and asserts via DB queries and journal - ---- - -## Next steps / TODOs for the team - -1. **Increase** unit test coverage of existing code using these patterns. -2. **Add `IDeterministicScheduler` abstractions** and implement test doubles. -3. **Create `tests/test-utilities` project** and convert a couple of existing tests to use it as examples. -4. **Define manifest schema** and commit a couple of versioned workflow definitions to `tests/definitions/`. -5. **Add one CI integration job** that uses Testcontainers (or similar) to run the integration suite against popular db providers (MySql, Postgres and Mongo, for example). -6. **Add telemetry and event collectors** to support in‑process assertions (easier than parsing journal text). ---- - -## Appendix: Quick checklist for writing a new test - -- [ ] Will this be a unit, integration or E2E test? Choose minimal scope. -- [ ] Can we avoid real time? If yes, use fake trigger. -- [ ] Will the test load a workflow definition artifact? Pin its version in the manifest. -- [ ] Will the test depend on DB state? Use a clean DB instance per test run. -- [ ] Use correlation ids for all invocations. -- [ ] Assert using deterministic state (instance id, DB record, or event) rather than `GetLatest`. -- [ ] On failure, capture logs and DB snapshot for diagnosis. - ---- diff --git a/doc/qa/test-guidelines-integrators.md b/doc/qa/test-guidelines-integrators.md deleted file mode 100644 index 8eddc7c25..000000000 --- a/doc/qa/test-guidelines-integrators.md +++ /dev/null @@ -1,59 +0,0 @@ - -## Goals / Non‑functional requirements - -1. **Deterministic tests** — tests should not be flaky and should produce the same results independent of circumstance. -2. **Fast feedback** — unit and integration tests should run quickly to support local development workflows and CI. -3. **Minimal reliance on real delays** — avoid `Task.Delay`, `Thread.Sleep` or real clocks except where unavoidable; prefer event-driven assertions. -4. **Environment portability** — tests should run in local dev, Docker or any other container CI environment with minimal changes. -5. **Version alignment** — workflow definition versions and test artifacts must be explicitly linked so tests refer to a specific workflow definition version. -6. **Failure simulation** — deterministic ways to simulate activity or host failures and assert correct recovery/compensation. - ---- - - -- **Bulk provisioning & isolation** — tests should support bulk import of workflow definitions for large-suite runs and ensure clean, isolated state per test. -- **End‑to‑end tests**: Deploy Elsa Server (or host app) in Docker/K8s with a real DB, then run workflows via REST and assert via durable traces (journal/DB/events). Keep E2E suite small and targeted. - -### Importing and publishing workflow definitions for testing workflows - -Two main patterns: - -**A. Code‑first registration (recommended for component tests)** -- Register workflows as code inside test setup. This is fast and avoids serialization roundtrips. -- Good for tests that validate runtime behavior without involving persistence or designer serialization. - -**B. Serialized definitions (recommended for integration & E2E tests)** -- Store JSON workflow definition artifacts in the `tests/definitions/` folder in the repo, commit them with semantic versions, and let tests import them into the engine via the same publish/import APIs used in production. -- Tests are responsible for *publishing* the definitions into the test host if the scenario requires persistence (e.g. testing versioned definitions or import behavior). - -**Which to use?** -- Component tests: code-first definitions or workflows created via designer and exported to JSON. -- Integration/E2E tests: use serialized artifacts to validate persistence, designer output and versioning behavior. - -### Bulk import - -- Implement a **test importer utility** that accepts a directory of workflow definition artifacts (JSON/YAML) and publishes them via the engine's public API or directly seeds the persistence store. The utility should: - - Validate schema and version. - - Report conflicts or duplicate IDs. - - Run in parallel but enforce deterministic ordering when versions matter. -- For very large import workloads, support an optimized DB seed path used only in tests (direct DB insert) to avoid the overhead of the full publish pipeline. Mark this as *test-only*. - - -### Working with Docker, env, K8s cluster deployments - -- **Test strategy split**: - - Local developer tests: use in‑process hosts and in‑memory/ephemeral DBs (SQLite in-memory or Testcontainers-based DB). Fast and deterministic. - - CI Docker Compose: spin up a lightweight containerized environment with the host app, a real DB (Postgres, SQL Server or Mongo) and optional message broker; use Testcontainers (or Docker Compose) to orchestrate in CI. - - K8s E2E: run a small suite that deploys a test namespace with Helm or apply manifests. Use ephemeral resources and ensure cleanup. Keep these tests in a separate CI stage. - -- **Use Testcontainers** (or equivalent) to provision ephemeral DBs/brokers in CI; this keeps environments close to production while still being isolated and reproducible. - -- **Configuration**: Keep environment variables and k8s manifests in `tests/ci/` and parametrize connection strings so tests can switch between in‑process and containerized runs. - -### CI recommendations - -- **Local unit tests**: run in `dotnet test` step (fast). Use in‑memory stores and determinism. -- **Integration tests**: run with Testcontainers (or similar) to provide real DB/broker. Run these in a separate CI job because they are slower. -- **K8s smoke tests**: optional separate stage. Deploy to ephemeral namespace via Helm and run a small suite of smoke E2E tests; tear down after. -- **Parallelization**: run multiple test matrices (DB providers) but avoid running heavy E2E jobs in parallel unless you have isolated resources. -- **Flaky test detection**: enable a flaky test retry policy for known non‑deterministic tests, but treat retries as signals to fix the underlying determinism problems. From 9ac605fbb570ea12e62bd13e486570225720e109 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 7 Oct 2025 09:03:16 +0200 Subject: [PATCH 31/40] Renaming guideline file --- Elsa.sln | 2 +- doc/qa/{test-guidelines-collaborators.md => test-guidelines.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/qa/{test-guidelines-collaborators.md => test-guidelines.md} (100%) diff --git a/Elsa.sln b/Elsa.sln index 2dec82de1..9fce3e932 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -300,7 +300,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.Integratio EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-4667-ADC2-37C62C9C2574}" ProjectSection(SolutionItems) = preProject - doc\qa\test-guidelines-collaborators.md = doc\qa\test-guidelines-collaborators.md + doc\qa\test-guidelines.md = doc\qa\test-guidelines.md EndProjectSection EndProject Global diff --git a/doc/qa/test-guidelines-collaborators.md b/doc/qa/test-guidelines.md similarity index 100% rename from doc/qa/test-guidelines-collaborators.md rename to doc/qa/test-guidelines.md From 9ab15d3d15cb216d7d602fdb04495d8337233442 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 9 Oct 2025 14:36:56 +0200 Subject: [PATCH 32/40] Reverting wrong commits --- src/apps/Elsa.Studio.Web/appsettings.json | 15 ++++++ .../VariableTypeDefinitionProvider.cs | 26 +++++----- .../TypeDefinitionDocumentRenderer.cs | 13 +---- .../TypeDefinitions/Services/TypeDescriber.cs | 19 +++---- .../Elsa.Http/Activities/SendHttpRequest.cs | 13 ++--- src/modules/Elsa.Http/Features/HttpFeature.cs | 36 +------------ .../SendHttpRequestActivityPortResolver.cs | 13 +---- .../Endpoints/Bookmarks/Resume/Endpoint.cs | 50 ++++++++++++------- .../Exceptions/InputEvaluationException.cs | 6 +++ ...cutionContextExtensions.InputEvaluation.cs | 31 ++++++------ 10 files changed, 92 insertions(+), 130 deletions(-) create mode 100644 src/apps/Elsa.Studio.Web/appsettings.json create mode 100644 src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs diff --git a/src/apps/Elsa.Studio.Web/appsettings.json b/src/apps/Elsa.Studio.Web/appsettings.json new file mode 100644 index 000000000..7e26f0ec0 --- /dev/null +++ b/src/apps/Elsa.Studio.Web/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "ElsaServer": { + "Url": "https://localhost:5001/elsa/api" + }, + "Hosting": { + "BasePath": "" + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs index f6bcd0b85..25f81738f 100644 --- a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs +++ b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs @@ -6,34 +6,32 @@ using Elsa.Expressions.JavaScript.TypeDefinitions.Models; using Elsa.Workflows.Management.Options; using JetBrains.Annotations; using Microsoft.Extensions.Options; - namespace Elsa.Expressions.JavaScript.Providers; - /// /// Produces s for variable types. /// -internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber) : TypeDefinitionProvider +[UsedImplicitly] +internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber, IOptions options) : TypeDefinitionProvider { protected override IEnumerable GetTypeDefinitions(TypeDefinitionContext context) { var excludedTypes = new Func[] { type => type == typeof(ExpandoObject), - type => typeof(IDictionary).IsAssignableFrom(type), - type => type == typeof(object) + type => type.IsPrimitive, + type => type.ContainsGenericParameters, + type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>), + type => type == typeof(object), + type => type == typeof(string) }; - var variables = context.WorkflowGraph.Workflow.Variables; - - var variableTypeQuery = - from variable in variables - let variableType = variable.GetVariableType() - where (variableType.IsClass || variableType.IsInterface || variableType.IsEnum) && !variableType.IsPrimitive && !excludedTypes.Any(x => x(variableType)) + var variableTypes = + from variableDescriptor in options.Value.VariableDescriptors + let variableType = variableDescriptor.Type + where (variableType.IsClass || variableType.IsInterface || variableType.IsEnum) && !excludedTypes.Any(x => x(variableType)) select variableType; - var variableTypes = variableTypeQuery.Distinct(); - - foreach (var variableType in variableTypes) + foreach (var variableType in variableTypes.Distinct()) { yield return typeDescriber.DescribeType(variableType); } diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs index fcc66000a..443886c65 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs @@ -1,9 +1,7 @@ using System.Text; using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Models; - namespace Elsa.Expressions.JavaScript.TypeDefinitions.Services; - /// public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer { @@ -11,19 +9,14 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer public string Render(TypeDefinitionsDocument document) { var stringBuilder = new StringBuilder(); - foreach (var functionDefinition in document.Functions) Render(functionDefinition, stringBuilder); - foreach (var typeDefinition in document.Types) Render(typeDefinition, stringBuilder); - foreach (var variableDefinition in document.Variables) Render(variableDefinition, stringBuilder); - return stringBuilder.ToString(); } - private void Render(FunctionDefinition functionDefinition, StringBuilder output) { var returnType = functionDefinition.ReturnType != null ? $": {functionDefinition.ReturnType}" : ""; @@ -35,11 +28,9 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer var returnType = functionDefinition.ReturnType != null ? $" => {functionDefinition.ReturnType}" : ""; output.AppendLine($"{functionDefinition.Name}: ({RenderParameters(functionDefinition.Parameters)}){returnType};"); } - private void Render(TypeDefinition typeDefinition, StringBuilder output) { output.AppendLine($"declare {typeDefinition.DeclarationKeyword} {typeDefinition.Name} {{"); - if (typeDefinition.DeclarationKeyword == "enum") { foreach (var property in typeDefinition.Properties) @@ -50,15 +41,13 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer foreach (var property in typeDefinition.Properties) Render(property, output); } - foreach (var method in typeDefinition.Methods) RenderMethod(method, output); - output.AppendLine("}"); } private void Render(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name}{(property.IsOptional ? "?" : "")}: {property.Type};"); - private void RenderEnumMember(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name} = \"{property.Name}\";"); + private void RenderEnumMember(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name} = \"{property.Name}\","); private void Render(VariableDefinition variable, StringBuilder output) => output.AppendLine($"declare var {variable.Name}: {variable.Type};"); string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}"; string RenderParameters(IEnumerable parameters) => string.Join(", ", parameters.Select(RenderParameter)); diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs index fc7079e98..9f4a141fb 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs @@ -1,17 +1,15 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using Elsa.Extensions; using Elsa.Expressions.JavaScript.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Models; - namespace Elsa.Expressions.JavaScript.TypeDefinitions.Services; - /// public class TypeDescriber : ITypeDescriber { private readonly ITypeAliasRegistry _typeAliasRegistry; - /// /// Constructor. /// @@ -30,17 +28,18 @@ public class TypeDescriber : ITypeDescriber Properties = GetPropertyDefinitions(type).DistinctBy(x => x.Name).ToList(), Methods = GetMethodDefinitions(type).DistinctBy(x => x.Name).ToList() }; - return typeDefinition; } - private IEnumerable GetMethodDefinitions(Type type) { if(type.IsEnum) yield break; - + #pragma warning disable IL2070 - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => !x.IsSpecialName).ToList(); + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Where(x => !x.IsSpecialName) + .Where(x => x.GetCustomAttribute() == null) + .ToList(); #pragma warning restore IL2070 foreach (var method in methods) @@ -53,11 +52,9 @@ public class TypeDescriber : ITypeDescriber }; } } - private IEnumerable GetMethodParameters(MethodInfo method) { var parameters = method.GetParameters(); - foreach (var parameter in parameters) { yield return new ParameterDefinition @@ -68,7 +65,6 @@ public class TypeDescriber : ITypeDescriber }; } } - private IEnumerable GetPropertyDefinitions([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) { // If the type is an enum, enumerate its members. @@ -83,12 +79,10 @@ public class TypeDescriber : ITypeDescriber IsOptional = false, }; } - yield break; } var properties = type.GetProperties(); - foreach (var property in properties) { yield return new PropertyDefinition @@ -99,7 +93,6 @@ public class TypeDescriber : ITypeDescriber }; } } - private static string GetDeclarationKeyword(Type type) => type switch { diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 7b27571c4..6a3b150fe 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -1,9 +1,7 @@ using System.Runtime.CompilerServices; using Elsa.Workflows; using Elsa.Workflows.Attributes; - namespace Elsa.Http; - /// /// Send an HTTP request. /// @@ -14,7 +12,6 @@ public class SendHttpRequest : SendHttpRequestBase public SendHttpRequest([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } - /// /// A list of expected status codes to handle and the corresponding activity to execute when the status code matches. /// @@ -23,21 +20,21 @@ public class SendHttpRequest : SendHttpRequestBase UIHint = "http-status-codes" )] public ICollection ExpectedStatusCodes { get; set; } = new List(); - /// /// The activity to execute when the HTTP status code does not match any of the expected status codes. /// [Port] public IActivity? UnmatchedStatusCode { get; set; } - /// /// The activity to execute when the HTTP request fails to connect. /// + [Port] public IActivity? FailedToConnect { get; set; } - + /// /// The activity to execute when the HTTP request times out. /// + [Port] public IActivity? Timeout { get; set; } /// @@ -47,22 +44,18 @@ public class SendHttpRequest : SendHttpRequestBase var statusCode = (int)response.StatusCode; var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode); var activity = matchingCase != null ? matchingCase.Activity : UnmatchedStatusCode; - await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync); } - /// protected override async ValueTask HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception) { await context.ScheduleActivityAsync(FailedToConnect, OnChildActivityCompletedAsync); } - /// protected override async ValueTask HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception) { await context.ScheduleActivityAsync(Timeout, OnChildActivityCompletedAsync); } - private async ValueTask OnChildActivityCompletedAsync(ActivityCompletedContext context) { await context.TargetContext.CompleteActivityAsync(); diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 3940688e3..a12022186 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -26,9 +26,7 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; - namespace Elsa.Http.Features; - /// /// Installs services related to HTTP services and activities. /// @@ -38,32 +36,26 @@ public class HttpFeature(IModule module) : FeatureBase(module) { private Func _httpEndpointRouteProvider = sp => sp.GetRequiredService(); private Func _httpEndpointBasePathProvider = sp => sp.GetRequiredService(); - /// /// A delegate to configure . /// public Action? ConfigureHttpOptions { get; set; } - /// /// A delegate to configure . /// public Action? ConfigureHttpFileCacheOptions { get; set; } - /// /// A delegate that is invoked when authorizing an inbound HTTP request. /// public Func HttpEndpointAuthorizationHandler { get; set; } = sp => sp.GetRequiredService(); - /// /// A delegate that is invoked when an HTTP workflow faults. /// public Func HttpEndpointWorkflowFaultHandler { get; set; } = sp => sp.GetRequiredService(); - /// /// A delegate to configure the . /// public Func ContentTypeProvider { get; set; } = _ => new FileExtensionContentTypeProvider(); - /// /// A delegate to configure the . /// @@ -75,7 +67,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) }; /// - /// A delegate to configure the used when by the activity. + /// A delegate to configure the used when by the and activities. /// public Action HttpClient { get; set; } = (_, _) => { }; @@ -83,7 +75,6 @@ public class HttpFeature(IModule module) : FeatureBase(module) /// A delegate to configure the for . /// public Action HttpClientBuilder { get; set; } = _ => { }; - /// /// A list of types to register with the service collection. /// @@ -92,7 +83,6 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HeaderHttpCorrelationIdSelector), typeof(QueryStringHttpCorrelationIdSelector) }; - /// /// A list of types to register with the service collection. /// @@ -101,12 +91,10 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HeaderHttpWorkflowInstanceIdSelector), typeof(QueryStringHttpWorkflowInstanceIdSelector) }; - public HttpFeature WithHttpEndpointRoutesProvider() where T : IHttpEndpointRoutesProvider { return WithHttpEndpointRoutesProvider(sp => sp.GetRequiredService()); } - public HttpFeature WithHttpEndpointRoutesProvider(Func httpEndpointRouteProvider) { _httpEndpointRouteProvider = httpEndpointRouteProvider; @@ -124,7 +112,6 @@ public class HttpFeature(IModule module) : FeatureBase(module) _httpEndpointBasePathProvider = httpEndpointBasePathProvider; return this; } - /// public override void Configure() { @@ -140,13 +127,10 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HttpFile), typeof(Downloadable) ], "HTTP"); - management.AddActivitiesFrom(); }); - Module.UseResilience(resilience => resilience.AddResilienceStrategyType()); } - /// public override void Apply() { @@ -155,15 +139,11 @@ public class HttpFeature(IModule module) : FeatureBase(module) options.BasePath = "/workflows"; options.BaseUrl = new Uri("http://localhost"); }); - var configureFileCacheOptions = ConfigureHttpFileCacheOptions ?? (options => { options.TimeToLive = TimeSpan.FromDays(7); }); - Services.Configure(configureOptions); Services.Configure(configureFileCacheOptions); - var httpClientBuilder = Services.AddHttpClient(HttpClient); HttpClientBuilder(httpClientBuilder); - Services .AddScoped() .AddScoped() @@ -172,31 +152,25 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddScoped() .AddScoped(ContentTypeProvider) .AddHttpContextAccessor() - // Handlers. .AddNotificationHandler() - // Content parsers. .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - // HTTP content factories. .AddScoped() .AddScoped() .AddScoped() .AddScoped() - // Activity property options providers. .AddScoped() .AddScoped() .AddScoped(_httpEndpointBasePathProvider) - // Port resolvers. .AddScoped() - // HTTP endpoint handlers. .AddScoped() .AddScoped() @@ -209,7 +183,6 @@ public class HttpFeature(IModule module) : FeatureBase(module) // Startup tasks. .AddStartupTask() - // Downloadable content handlers. .AddScoped() .AddScoped() @@ -220,28 +193,21 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddScoped() .AddScoped() .AddScoped() - //Trigger payload validators. .AddTriggerPayloadValidator() - // File caches. .AddScoped(FileCache) .AddScoped() - // AuthenticationBasedHttpEndpointAuthorizationHandler requires Authorization services. // We could consider creating a separate module for installing authorization services. .AddAuthorization(); - // HTTP clients. Services.AddHttpClient(); - // Add selectors. foreach (var httpCorrelationIdSelectorType in HttpCorrelationIdSelectorTypes) Services.AddScoped(typeof(IHttpCorrelationIdSelector), httpCorrelationIdSelectorType); - foreach (var httpWorkflowInstanceIdSelectorType in HttpWorkflowInstanceIdSelectorTypes) Services.AddScoped(typeof(IHttpWorkflowInstanceIdSelector), httpWorkflowInstanceIdSelectorType); - Services.Configure(options => { options.AddTypeAlias("HttpRequest"); diff --git a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs index 92dcd991d..decd7e90d 100644 --- a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs +++ b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs @@ -1,8 +1,6 @@ using Elsa.Workflows; using Elsa.Workflows.Models; - namespace Elsa.Http.PortResolvers; - /// /// Returns a list of outbound activities for a given activity's expected status codes. /// @@ -10,32 +8,25 @@ public class SendHttpRequestActivityResolver : IActivityResolver { /// public int Priority => 0; - /// public bool GetSupportsActivity(IActivity activity) => activity is SendHttpRequest; - - /// public ValueTask> GetActivityPortsAsync(IActivity activity, CancellationToken cancellationToken = default) { - IEnumerable ports = GetPortsInternal(activity); - return new ValueTask>(ports); + var ports = GetPortsInternal(activity); + return new(ports); } private IEnumerable GetPortsInternal(IActivity activity) { var sendHttpRequest = (SendHttpRequest)activity; var cases = sendHttpRequest.ExpectedStatusCodes.Where(x => x.Activity != null); - foreach (var @case in cases) yield return ActivityPort.FromActivity(@case.Activity!, @case.StatusCode.ToString()); - if (sendHttpRequest.Timeout != null) yield return ActivityPort.FromActivity(sendHttpRequest.Timeout, nameof(SendHttpRequest.Timeout)); - if (sendHttpRequest.FailedToConnect != null) yield return ActivityPort.FromActivity(sendHttpRequest.FailedToConnect, nameof(SendHttpRequest.FailedToConnect)); - if (sendHttpRequest.UnmatchedStatusCode != null) yield return ActivityPort.FromActivity(sendHttpRequest.UnmatchedStatusCode, nameof(SendHttpRequest.UnmatchedStatusCode)); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs index 832b60f59..2583e180b 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs @@ -4,14 +4,12 @@ using Elsa.Workflows.Runtime; using FastEndpoints; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; - namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume; - /// /// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token. /// [PublicAPI] -internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint +internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResumer, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint { /// public override void Configure() @@ -20,25 +18,27 @@ internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, Verbs(Http.GET, Http.POST); AllowAnonymous(); } - /// public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { var token = Query("t")!; + var asynchronous = Query("async", false); if (!tokenService.TryDecryptToken(token, out var payload)) AddError("Invalid token."); - var input = HttpContext.Request.Method == HttpMethods.Post ? request.Input : GetInputFromQueryString(); - if (ValidationFailed) { await Send.ErrorsAsync(cancellation: cancellationToken); return; } - - await ResumeBookmarkedWorkflowAsync(payload, input, cancellationToken); - + + // Some clients, like Blazor, may prematurely cancel their request upon navigation away from the page. + // In this case, we don't want to cancel the workflow execution. + // We need to better understand the conditions that cause this. + var workflowCancellationToken = CancellationToken.None; + await ResumeBookmarkedWorkflowAsync(payload, input, asynchronous, workflowCancellationToken); + if (!HttpContext.Response.HasStarted) await Send.OkAsync(cancellationToken); } @@ -48,7 +48,6 @@ internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, var inputJson = Query("in", false); if (string.IsNullOrWhiteSpace(inputJson)) return null; - try { return serializer.Deserialize>(inputJson); @@ -59,21 +58,36 @@ internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, return null; } } - - private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary? input, CancellationToken cancellationToken) + + private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary? input, bool asynchronous, CancellationToken cancellationToken) { var bookmarkId = tokenPayload.BookmarkId; var workflowInstanceId = tokenPayload.WorkflowInstanceId; - var item = new NewBookmarkQueueItem + + if (asynchronous) + { + var item = new NewBookmarkQueueItem + { + BookmarkId = bookmarkId, + WorkflowInstanceId = workflowInstanceId, + Options = new() + { + Input = input + } + }; + + await bookmarkQueue.EnqueueAsync(item, cancellationToken); + return; + } + + var resumeRequest = new ResumeBookmarkRequest { BookmarkId = bookmarkId, WorkflowInstanceId = workflowInstanceId, - Options = new() - { - Input = input - } + Input = input }; - await bookmarkQueue.EnqueueAsync(item, cancellationToken); + + await workflowResumer.ResumeAsync(resumeRequest, cancellationToken); } } diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs new file mode 100644 index 000000000..b067f53e2 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Exceptions/InputEvaluationException.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows.Exceptions; + +public class InputEvaluationException(string inputName, string message, Exception exception) : Exception(message, exception) +{ + public string InputName { get; } = inputName; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index 093ede7f1..c10a9ac38 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -3,11 +3,11 @@ using Elsa.Expressions.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Workflows; +using Elsa.Workflows.Exceptions; using Elsa.Workflows.Models; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; - public static partial class ActivityExecutionContextExtensions { /// @@ -17,14 +17,11 @@ public static partial class ActivityExecutionContextExtensions { var activityDescriptor = context.ActivityDescriptor; var inputDescriptors = activityDescriptor.Inputs.Where(x => x.AutoEvaluate).ToList(); - // Evaluate inputs. foreach (var inputDescriptor in inputDescriptors) await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor); - context.SetHasEvaluatedProperties(); } - /// /// Evaluates the specified input property of the activity. /// @@ -34,7 +31,6 @@ public static partial class ActivityExecutionContextExtensions var input = await EvaluateInputPropertyAsync(context, inputName); return input.ConvertTo(); } - /// /// Evaluates a specific input property of the activity. /// @@ -44,13 +40,10 @@ public static partial class ActivityExecutionContextExtensions var activityRegistryLookup = context.GetRequiredService(); var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type) ?? throw new Exception("Activity descriptor not found"); var inputDescriptor = activityDescriptor.GetWrappedInputPropertyDescriptor(activity, inputName); - if (inputDescriptor == null) throw new Exception($"No input with name {inputName} could be found"); - return await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor); } - /// /// Evaluates the specified input and sets the result in the activity execution context's memory space. /// @@ -66,19 +59,29 @@ public static partial class ActivityExecutionContextExtensions memoryBlockReference.Set(context, value); return value; } - + private static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) + { + try + { + return await EvaluateInputPropertyCoreAsync(context, activityDescriptor, inputDescriptor); + } + catch (Exception e) + { + throw new InputEvaluationException(inputDescriptor.Name, $"Failed to evaluate activity input '{inputDescriptor.Name}'", e); + } + } + + private static async Task EvaluateInputPropertyCoreAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { var activity = context.Activity; var defaultValue = inputDescriptor.DefaultValue; var value = defaultValue; var input = inputDescriptor.ValueGetter(activity); var identityGenerator = context.GetRequiredService(); - if (inputDescriptor.IsWrapped) { var wrappedInput = (Input?)input; - if (defaultValue != null && wrappedInput == null) { var typedInput = typeof(Input<>).MakeGenericType(inputDescriptor.Type); @@ -94,7 +97,6 @@ public static partial class ActivityExecutionContextExtensions var expressionEvaluator = context.GetRequiredService(); var expressionExecutionContext = context.ExpressionExecutionContext; var inputEvaluatorType = inputDescriptor.EvaluatorType ?? typeof(DefaultActivityInputEvaluator); - if (wrappedInput?.Expression != null) { var inputEvaluator = (IActivityInputEvaluator)context.GetRequiredService(inputEvaluatorType); @@ -102,9 +104,7 @@ public static partial class ActivityExecutionContextExtensions value = await inputEvaluator.EvaluateAsync(inputEvaluatorContext); } } - var memoryReference = wrappedInput?.MemoryBlockReference(); - if (memoryReference != null) { // When input is created from an activity provider, there may be no memory block reference ID. @@ -119,9 +119,7 @@ public static partial class ActivityExecutionContextExtensions { value = input; } - await StoreInputValueAsync(context, inputDescriptor, value!); - return value; } @@ -138,7 +136,6 @@ public static partial class ActivityExecutionContextExtensions // var filterResult = await manager.RunFiltersAsync(filterContext); context.ActivityState[inputDescriptor.Name] = value; } - return Task.CompletedTask; } } \ No newline at end of file From 8ec29358b524668c1b59f864973ad21c06a07fb3 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 9 Oct 2025 14:45:33 +0200 Subject: [PATCH 33/40] Equalizing with 3.6.0 --- .../VariableTypeDefinitionProvider.cs | 2 ++ .../TypeDefinitionDocumentRenderer.cs | 11 ++++++ .../TypeDefinitions/Services/TypeDescriber.cs | 13 ++++++- .../Elsa.Http/Activities/SendHttpRequest.cs | 11 +++++- src/modules/Elsa.Http/Features/HttpFeature.cs | 34 +++++++++++++++++++ .../SendHttpRequestActivityPortResolver.cs | 9 +++++ .../Endpoints/Bookmarks/Resume/Endpoint.cs | 14 +++++--- ...cutionContextExtensions.InputEvaluation.cs | 20 +++++++++-- 8 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs index 25f81738f..20b78ef9a 100644 --- a/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs +++ b/src/modules/Elsa.Expressions.JavaScript/Providers/VariableTypeDefinitionProvider.cs @@ -6,7 +6,9 @@ using Elsa.Expressions.JavaScript.TypeDefinitions.Models; using Elsa.Workflows.Management.Options; using JetBrains.Annotations; using Microsoft.Extensions.Options; + namespace Elsa.Expressions.JavaScript.Providers; + /// /// Produces s for variable types. /// diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs index 443886c65..cbd84d69d 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs @@ -1,7 +1,9 @@ using System.Text; using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Models; + namespace Elsa.Expressions.JavaScript.TypeDefinitions.Services; + /// public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer { @@ -9,14 +11,19 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer public string Render(TypeDefinitionsDocument document) { var stringBuilder = new StringBuilder(); + foreach (var functionDefinition in document.Functions) Render(functionDefinition, stringBuilder); + foreach (var typeDefinition in document.Types) Render(typeDefinition, stringBuilder); + foreach (var variableDefinition in document.Variables) Render(variableDefinition, stringBuilder); + return stringBuilder.ToString(); } + private void Render(FunctionDefinition functionDefinition, StringBuilder output) { var returnType = functionDefinition.ReturnType != null ? $": {functionDefinition.ReturnType}" : ""; @@ -28,9 +35,11 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer var returnType = functionDefinition.ReturnType != null ? $" => {functionDefinition.ReturnType}" : ""; output.AppendLine($"{functionDefinition.Name}: ({RenderParameters(functionDefinition.Parameters)}){returnType};"); } + private void Render(TypeDefinition typeDefinition, StringBuilder output) { output.AppendLine($"declare {typeDefinition.DeclarationKeyword} {typeDefinition.Name} {{"); + if (typeDefinition.DeclarationKeyword == "enum") { foreach (var property in typeDefinition.Properties) @@ -41,8 +50,10 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer foreach (var property in typeDefinition.Properties) Render(property, output); } + foreach (var method in typeDefinition.Methods) RenderMethod(method, output); + output.AppendLine("}"); } diff --git a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs index 9f4a141fb..ef2a395a1 100644 --- a/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs +++ b/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDescriber.cs @@ -5,11 +5,14 @@ using Elsa.Extensions; using Elsa.Expressions.JavaScript.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts; using Elsa.Expressions.JavaScript.TypeDefinitions.Models; + namespace Elsa.Expressions.JavaScript.TypeDefinitions.Services; + /// public class TypeDescriber : ITypeDescriber { private readonly ITypeAliasRegistry _typeAliasRegistry; + /// /// Constructor. /// @@ -28,13 +31,15 @@ public class TypeDescriber : ITypeDescriber Properties = GetPropertyDefinitions(type).DistinctBy(x => x.Name).ToList(), Methods = GetMethodDefinitions(type).DistinctBy(x => x.Name).ToList() }; + return typeDefinition; } + private IEnumerable GetMethodDefinitions(Type type) { if(type.IsEnum) yield break; - + #pragma warning disable IL2070 var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) .Where(x => !x.IsSpecialName) @@ -52,9 +57,11 @@ public class TypeDescriber : ITypeDescriber }; } } + private IEnumerable GetMethodParameters(MethodInfo method) { var parameters = method.GetParameters(); + foreach (var parameter in parameters) { yield return new ParameterDefinition @@ -65,6 +72,7 @@ public class TypeDescriber : ITypeDescriber }; } } + private IEnumerable GetPropertyDefinitions([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) { // If the type is an enum, enumerate its members. @@ -79,10 +87,12 @@ public class TypeDescriber : ITypeDescriber IsOptional = false, }; } + yield break; } var properties = type.GetProperties(); + foreach (var property in properties) { yield return new PropertyDefinition @@ -93,6 +103,7 @@ public class TypeDescriber : ITypeDescriber }; } } + private static string GetDeclarationKeyword(Type type) => type switch { diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 6a3b150fe..e0a564259 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -1,7 +1,9 @@ using System.Runtime.CompilerServices; using Elsa.Workflows; using Elsa.Workflows.Attributes; + namespace Elsa.Http; + /// /// Send an HTTP request. /// @@ -12,6 +14,7 @@ public class SendHttpRequest : SendHttpRequestBase public SendHttpRequest([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } + /// /// A list of expected status codes to handle and the corresponding activity to execute when the status code matches. /// @@ -20,17 +23,19 @@ public class SendHttpRequest : SendHttpRequestBase UIHint = "http-status-codes" )] public ICollection ExpectedStatusCodes { get; set; } = new List(); + /// /// The activity to execute when the HTTP status code does not match any of the expected status codes. /// [Port] public IActivity? UnmatchedStatusCode { get; set; } + /// /// The activity to execute when the HTTP request fails to connect. /// [Port] public IActivity? FailedToConnect { get; set; } - + /// /// The activity to execute when the HTTP request times out. /// @@ -44,18 +49,22 @@ public class SendHttpRequest : SendHttpRequestBase var statusCode = (int)response.StatusCode; var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode); var activity = matchingCase != null ? matchingCase.Activity : UnmatchedStatusCode; + await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync); } + /// protected override async ValueTask HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception) { await context.ScheduleActivityAsync(FailedToConnect, OnChildActivityCompletedAsync); } + /// protected override async ValueTask HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception) { await context.ScheduleActivityAsync(Timeout, OnChildActivityCompletedAsync); } + private async ValueTask OnChildActivityCompletedAsync(ActivityCompletedContext context) { await context.TargetContext.CompleteActivityAsync(); diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index a12022186..b64caa324 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -26,7 +26,9 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; + namespace Elsa.Http.Features; + /// /// Installs services related to HTTP services and activities. /// @@ -36,26 +38,32 @@ public class HttpFeature(IModule module) : FeatureBase(module) { private Func _httpEndpointRouteProvider = sp => sp.GetRequiredService(); private Func _httpEndpointBasePathProvider = sp => sp.GetRequiredService(); + /// /// A delegate to configure . /// public Action? ConfigureHttpOptions { get; set; } + /// /// A delegate to configure . /// public Action? ConfigureHttpFileCacheOptions { get; set; } + /// /// A delegate that is invoked when authorizing an inbound HTTP request. /// public Func HttpEndpointAuthorizationHandler { get; set; } = sp => sp.GetRequiredService(); + /// /// A delegate that is invoked when an HTTP workflow faults. /// public Func HttpEndpointWorkflowFaultHandler { get; set; } = sp => sp.GetRequiredService(); + /// /// A delegate to configure the . /// public Func ContentTypeProvider { get; set; } = _ => new FileExtensionContentTypeProvider(); + /// /// A delegate to configure the . /// @@ -75,6 +83,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) /// A delegate to configure the for . /// public Action HttpClientBuilder { get; set; } = _ => { }; + /// /// A list of types to register with the service collection. /// @@ -83,6 +92,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HeaderHttpCorrelationIdSelector), typeof(QueryStringHttpCorrelationIdSelector) }; + /// /// A list of types to register with the service collection. /// @@ -91,10 +101,12 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HeaderHttpWorkflowInstanceIdSelector), typeof(QueryStringHttpWorkflowInstanceIdSelector) }; + public HttpFeature WithHttpEndpointRoutesProvider() where T : IHttpEndpointRoutesProvider { return WithHttpEndpointRoutesProvider(sp => sp.GetRequiredService()); } + public HttpFeature WithHttpEndpointRoutesProvider(Func httpEndpointRouteProvider) { _httpEndpointRouteProvider = httpEndpointRouteProvider; @@ -112,6 +124,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) _httpEndpointBasePathProvider = httpEndpointBasePathProvider; return this; } + /// public override void Configure() { @@ -127,10 +140,13 @@ public class HttpFeature(IModule module) : FeatureBase(module) typeof(HttpFile), typeof(Downloadable) ], "HTTP"); + management.AddActivitiesFrom(); }); + Module.UseResilience(resilience => resilience.AddResilienceStrategyType()); } + /// public override void Apply() { @@ -139,11 +155,15 @@ public class HttpFeature(IModule module) : FeatureBase(module) options.BasePath = "/workflows"; options.BaseUrl = new Uri("http://localhost"); }); + var configureFileCacheOptions = ConfigureHttpFileCacheOptions ?? (options => { options.TimeToLive = TimeSpan.FromDays(7); }); + Services.Configure(configureOptions); Services.Configure(configureFileCacheOptions); + var httpClientBuilder = Services.AddHttpClient(HttpClient); HttpClientBuilder(httpClientBuilder); + Services .AddScoped() .AddScoped() @@ -152,25 +172,31 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddScoped() .AddScoped(ContentTypeProvider) .AddHttpContextAccessor() + // Handlers. .AddNotificationHandler() + // Content parsers. .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + // HTTP content factories. .AddScoped() .AddScoped() .AddScoped() .AddScoped() + // Activity property options providers. .AddScoped() .AddScoped() .AddScoped(_httpEndpointBasePathProvider) + // Port resolvers. .AddScoped() + // HTTP endpoint handlers. .AddScoped() .AddScoped() @@ -183,6 +209,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) // Startup tasks. .AddStartupTask() + // Downloadable content handlers. .AddScoped() .AddScoped() @@ -193,21 +220,28 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddScoped() .AddScoped() .AddScoped() + //Trigger payload validators. .AddTriggerPayloadValidator() + // File caches. .AddScoped(FileCache) .AddScoped() + // AuthenticationBasedHttpEndpointAuthorizationHandler requires Authorization services. // We could consider creating a separate module for installing authorization services. .AddAuthorization(); + // HTTP clients. Services.AddHttpClient(); + // Add selectors. foreach (var httpCorrelationIdSelectorType in HttpCorrelationIdSelectorTypes) Services.AddScoped(typeof(IHttpCorrelationIdSelector), httpCorrelationIdSelectorType); + foreach (var httpWorkflowInstanceIdSelectorType in HttpWorkflowInstanceIdSelectorTypes) Services.AddScoped(typeof(IHttpWorkflowInstanceIdSelector), httpWorkflowInstanceIdSelectorType); + Services.Configure(options => { options.AddTypeAlias("HttpRequest"); diff --git a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs index decd7e90d..82a3b91c2 100644 --- a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs +++ b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs @@ -1,6 +1,8 @@ using Elsa.Workflows; using Elsa.Workflows.Models; + namespace Elsa.Http.PortResolvers; + /// /// Returns a list of outbound activities for a given activity's expected status codes. /// @@ -8,8 +10,11 @@ public class SendHttpRequestActivityResolver : IActivityResolver { /// public int Priority => 0; + /// public bool GetSupportsActivity(IActivity activity) => activity is SendHttpRequest; + + /// public ValueTask> GetActivityPortsAsync(IActivity activity, CancellationToken cancellationToken = default) { @@ -21,12 +26,16 @@ public class SendHttpRequestActivityResolver : IActivityResolver { var sendHttpRequest = (SendHttpRequest)activity; var cases = sendHttpRequest.ExpectedStatusCodes.Where(x => x.Activity != null); + foreach (var @case in cases) yield return ActivityPort.FromActivity(@case.Activity!, @case.StatusCode.ToString()); + if (sendHttpRequest.Timeout != null) yield return ActivityPort.FromActivity(sendHttpRequest.Timeout, nameof(SendHttpRequest.Timeout)); + if (sendHttpRequest.FailedToConnect != null) yield return ActivityPort.FromActivity(sendHttpRequest.FailedToConnect, nameof(SendHttpRequest.FailedToConnect)); + if (sendHttpRequest.UnmatchedStatusCode != null) yield return ActivityPort.FromActivity(sendHttpRequest.UnmatchedStatusCode, nameof(SendHttpRequest.UnmatchedStatusCode)); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs index 2583e180b..0943545f0 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs @@ -4,7 +4,9 @@ using Elsa.Workflows.Runtime; using FastEndpoints; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; + namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume; + /// /// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token. /// @@ -18,6 +20,7 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum Verbs(Http.GET, Http.POST); AllowAnonymous(); } + /// public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { @@ -26,19 +29,21 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum if (!tokenService.TryDecryptToken(token, out var payload)) AddError("Invalid token."); + var input = HttpContext.Request.Method == HttpMethods.Post ? request.Input : GetInputFromQueryString(); + if (ValidationFailed) { await Send.ErrorsAsync(cancellation: cancellationToken); return; } - + // Some clients, like Blazor, may prematurely cancel their request upon navigation away from the page. // In this case, we don't want to cancel the workflow execution. // We need to better understand the conditions that cause this. var workflowCancellationToken = CancellationToken.None; await ResumeBookmarkedWorkflowAsync(payload, input, asynchronous, workflowCancellationToken); - + if (!HttpContext.Response.HasStarted) await Send.OkAsync(cancellationToken); } @@ -48,6 +53,7 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum var inputJson = Query("in", false); if (string.IsNullOrWhiteSpace(inputJson)) return null; + try { return serializer.Deserialize>(inputJson); @@ -58,7 +64,7 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum return null; } } - + private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary? input, bool asynchronous, CancellationToken cancellationToken) { var bookmarkId = tokenPayload.BookmarkId; @@ -86,7 +92,7 @@ internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResum WorkflowInstanceId = workflowInstanceId, Input = input }; - + await workflowResumer.ResumeAsync(resumeRequest, cancellationToken); } } diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index c10a9ac38..aee9509a3 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -8,6 +8,7 @@ using Elsa.Workflows.Models; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; + public static partial class ActivityExecutionContextExtensions { /// @@ -17,11 +18,14 @@ public static partial class ActivityExecutionContextExtensions { var activityDescriptor = context.ActivityDescriptor; var inputDescriptors = activityDescriptor.Inputs.Where(x => x.AutoEvaluate).ToList(); + // Evaluate inputs. foreach (var inputDescriptor in inputDescriptors) await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor); + context.SetHasEvaluatedProperties(); } + /// /// Evaluates the specified input property of the activity. /// @@ -31,6 +35,7 @@ public static partial class ActivityExecutionContextExtensions var input = await EvaluateInputPropertyAsync(context, inputName); return input.ConvertTo(); } + /// /// Evaluates a specific input property of the activity. /// @@ -40,10 +45,13 @@ public static partial class ActivityExecutionContextExtensions var activityRegistryLookup = context.GetRequiredService(); var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type) ?? throw new Exception("Activity descriptor not found"); var inputDescriptor = activityDescriptor.GetWrappedInputPropertyDescriptor(activity, inputName); + if (inputDescriptor == null) throw new Exception($"No input with name {inputName} could be found"); + return await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor); } + /// /// Evaluates the specified input and sets the result in the activity execution context's memory space. /// @@ -59,7 +67,7 @@ public static partial class ActivityExecutionContextExtensions memoryBlockReference.Set(context, value); return value; } - + private static async Task EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { try @@ -71,7 +79,7 @@ public static partial class ActivityExecutionContextExtensions throw new InputEvaluationException(inputDescriptor.Name, $"Failed to evaluate activity input '{inputDescriptor.Name}'", e); } } - + private static async Task EvaluateInputPropertyCoreAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor) { var activity = context.Activity; @@ -79,9 +87,11 @@ public static partial class ActivityExecutionContextExtensions var value = defaultValue; var input = inputDescriptor.ValueGetter(activity); var identityGenerator = context.GetRequiredService(); + if (inputDescriptor.IsWrapped) { var wrappedInput = (Input?)input; + if (defaultValue != null && wrappedInput == null) { var typedInput = typeof(Input<>).MakeGenericType(inputDescriptor.Type); @@ -97,6 +107,7 @@ public static partial class ActivityExecutionContextExtensions var expressionEvaluator = context.GetRequiredService(); var expressionExecutionContext = context.ExpressionExecutionContext; var inputEvaluatorType = inputDescriptor.EvaluatorType ?? typeof(DefaultActivityInputEvaluator); + if (wrappedInput?.Expression != null) { var inputEvaluator = (IActivityInputEvaluator)context.GetRequiredService(inputEvaluatorType); @@ -104,7 +115,9 @@ public static partial class ActivityExecutionContextExtensions value = await inputEvaluator.EvaluateAsync(inputEvaluatorContext); } } + var memoryReference = wrappedInput?.MemoryBlockReference(); + if (memoryReference != null) { // When input is created from an activity provider, there may be no memory block reference ID. @@ -119,7 +132,9 @@ public static partial class ActivityExecutionContextExtensions { value = input; } + await StoreInputValueAsync(context, inputDescriptor, value!); + return value; } @@ -136,6 +151,7 @@ public static partial class ActivityExecutionContextExtensions // var filterResult = await manager.RunFiltersAsync(filterContext); context.ActivityState[inputDescriptor.Name] = value; } + return Task.CompletedTask; } } \ No newline at end of file From c9be7f0d13a88553b154b087073716870578ef97 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Thu, 9 Oct 2025 14:49:00 +0200 Subject: [PATCH 34/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- 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 618e39ee4..5d75a5ec6 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -11,7 +11,7 @@ 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) with minimal noise. +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. From e9ae53c4c672812bc902c1e211051457bfbcc950 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Thu, 9 Oct 2025 14:49:49 +0200 Subject: [PATCH 35/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- 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 5d75a5ec6..7cfef15cf 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -20,7 +20,7 @@ For contributors, tests are the first line of code review: they must document in ## 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 (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. +- **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. From f1e69a153618e043401e2a5b2ab629e0084373f3 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Thu, 9 Oct 2025 16:55:12 +0200 Subject: [PATCH 36/40] Small wording improvement --- 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 7cfef15cf..2edc1e97e 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -36,7 +36,7 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o **5-Minute Checklist:** - [ ] Read the relevant section below for your change type: - - Changed activity logic? → See [Activities](#activities) + - 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 From d0aafe6d4ebb22562874424f6109d3441e85b473 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 15:02:38 +0200 Subject: [PATCH 37/40] 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 f6caad19fb9229908996048902e22db7961018aa Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Mon, 13 Oct 2025 16:11:10 +0200 Subject: [PATCH 38/40] Update doc/qa/test-guidelines.md Co-authored-by: Sipke Schoorstra --- doc/qa/test-guidelines.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 2edc1e97e..3183cbf28 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -210,25 +210,9 @@ public async Task Workflow_Persists_Instance_And_Journal() .UseRealPersistenceForTests() .Build(); - var runner = sp.GetRequiredService(); - var store = sp.GetRequiredService(); - - var result = await runner.RunAsync(workflow); - var instanceId = result.WorkflowInstance!.Id; - - // deterministic lookup: query store until terminal state - var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10); - WorkflowInstance? instance = null; - while (DateTime.UtcNow < deadline) - { - instance = await store.FindByIdAsync(instanceId); - if (instance is not null && instance.Status is WorkflowStatus.Finished or WorkflowStatus.Faulted) - break; - await Task.Delay(150); - } - - instance.Should().NotBeNull(); - instance!.Status.Should().Be(WorkflowStatus.Finished); + var runner = sp.GetRequiredService(); +var result = await runner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(someDefinitionId, VersionOptions.Published)); + result.WorkflowExecutionContext.Status.Should().Be(WorkflowStatus.Finished); } ``` --- From bd6aea44988d8d94a1274574c00009d0977fe619 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 16:28:33 +0200 Subject: [PATCH 39/40] Final Improvements --- doc/qa/test-guidelines.md | 71 ++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index 2edc1e97e..68e0464ec 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -17,6 +17,22 @@ For contributors, tests are the first line of code review: they must document in --- +## 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. @@ -29,7 +45,7 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o ## Quick Start for Contributors -**Before you write a test:** +**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)) @@ -47,9 +63,9 @@ 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. +- **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). -- **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. +- **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). --- @@ -78,7 +94,7 @@ await serviceProvider.RunActivityAsync(writeLine); Assert.Equal("Hello world!", capturingTextWriter.Lines.Single()); ``` -#### **Integration tests (recommended if activity participates in workflows):** +#### **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. @@ -117,30 +133,34 @@ var resumed = await runner.RunAsync(workflowInstance); Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status); ``` -### Persistence & Serialization - -#### Integration tests:** -- Import a JSON workflow definition via the same serializers used by the engine (the test helper [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) demonstrates this pattern). Run the workflow through [`IWorkflowRunner`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) to validate deserialization + execution. --- ## 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 tests | -| `PopulateRegistriesAsync` | Register types for JSON deserialization | Loading JSON workflows | -| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) | -| `RunWorkflowUntilEndAsync` | Drive workflow to completion | Complex resumption scenarios | +| 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 | --- ## 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 invoker/scheduler/bookmarks or workflow composition?** → Integration test using [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and a small workflow. If persistence semantics change, add component tests. -3. **Change touches persistence/serializers or requires durable evidence (journal, bookmarks)?** → Component tests against [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). +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 it’s about **internal logic**, write a **unit** test. +- If it’s about **collaboration between components**, write an **integration** test. +- If it’s 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. @@ -156,13 +176,9 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu --- ## Failure testing (faults & incidents) - -- **Unit test [`Fault`](../../src/modules/Elsa.Workflows.Core/Activities/Fault.cs) activity**: instantiate the [`Fault`](../../src/modules/Elsa.Workflows.Core/Activities/Fault.cs) activity class and assert the expected exception/behavior. - **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. -**Tip:** tests that simulate host restart should recreate the service provider but reuse the same persistence store instance (in-memory DB configured at the test scope or repo test fixtures). This proves the engine resumes from persisted state. - --- ## Practical test recipes & snippets (copy/paste-ready) @@ -207,13 +223,12 @@ public async Task Workflow_With_MyActivity_Completes() public async Task Workflow_Persists_Instance_And_Journal() { var sp = new TestApplicationBuilder(testOutput) - .UseRealPersistenceForTests() .Build(); var runner = sp.GetRequiredService(); var store = sp.GetRequiredService(); - - var result = await runner.RunAsync(workflow); + var client = WorkflowServer.CreateApiClient(); + using var result = await client.ExecuteAsync(workflowId); var instanceId = result.WorkflowInstance!.Id; // deterministic lookup: query store until terminal state @@ -235,8 +250,8 @@ public async Task Workflow_Persists_Instance_And_Journal() ## FAQ (quick pointers) -**Q: How do I import workflow definitions in tests?** -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. +**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: 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). @@ -245,7 +260,7 @@ A: Prefer [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Con 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: Not yet — the repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and integration helpers that cover most scenarios. If you find many duplicated poll loops, open an issue requesting a canonical `WaitForCompletion` helper in `test/shared`. +A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) and integration helpers that cover all necessary scenarios. --- From ab7d0f6d55f7c643563c91654c8d55fddd26dfbd Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 13 Oct 2025 17:07:56 +0200 Subject: [PATCH 40/40] 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).