Add StellaOps.Workflow engine: 14 libraries, WebService, 8 test projects
Extract product-agnostic workflow engine from Ablera.Serdica.Workflow into standalone StellaOps.Workflow.* libraries targeting net10.0. Libraries (14): - Contracts, Abstractions (compiler, decompiler, expression runtime) - Engine (execution, signaling, scheduling, projections, hosted services) - ElkSharp (generic graph layout algorithm) - Renderer.ElkSharp, Renderer.ElkJs, Renderer.Msagl, Renderer.Svg - Signaling.Redis, Signaling.OracleAq - DataStore.MongoDB, DataStore.PostgreSQL, DataStore.Oracle WebService: ASP.NET Core Minimal API with 22 endpoints Tests (8 projects, 109 tests pass): - Engine.Tests (105 pass), WebService.Tests (4 E2E pass) - Renderer.Tests, DataStore.MongoDB/Oracle/PostgreSQL.Tests - Signaling.Redis.Tests, IntegrationTests.Shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
docs/workflow/tutorials/09-testing/README.md
Normal file
29
docs/workflow/tutorials/09-testing/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Tutorial 9: Testing Your Workflow
|
||||
|
||||
Write unit tests for workflows using `RecordingSerdicaLegacyRabbitTransport` and `TechnicalStyleWorkflowTestHelpers`.
|
||||
|
||||
## Test Setup Pattern
|
||||
|
||||
1. Create a recording transport with pre-configured responses
|
||||
2. Build a test service provider via `TechnicalStyleWorkflowTestHelpers.CreateServiceProvider`
|
||||
3. Resolve `WorkflowRuntimeService` from DI
|
||||
4. Call `StartWorkflowAsync` with test payload
|
||||
5. Assert: tasks created, transport calls made, state values correct
|
||||
6. Optionally complete tasks and verify downstream behavior
|
||||
|
||||
## What to Test
|
||||
|
||||
| Scenario | Approach |
|
||||
|----------|----------|
|
||||
| Workflow starts correctly | Assert single open task after start |
|
||||
| Service calls made in order | `transport.Invocations.Select(x => x.Command).Should().Equal(...)` |
|
||||
| Rejection flow | Complete task with `"answer": "reject"`, verify cancel call |
|
||||
| Approval flow | Complete with `"answer": "approve"`, verify conversion calls |
|
||||
| Operations failure re-opens task | Check same task re-appears after operations return `passed: false` |
|
||||
| Sub-workflow creates child tasks | Query tasks by child workflow name |
|
||||
| Business reference set | `startResponse.BusinessReference.Key.Should().Be(...)` |
|
||||
|
||||
## C#-Only Tutorial
|
||||
|
||||
- [C# Test Examples](csharp/)
|
||||
|
||||
196
docs/workflow/tutorials/09-testing/csharp/WorkflowTests.cs
Normal file
196
docs/workflow/tutorials/09-testing/csharp/WorkflowTests.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using WorkflowEngine.Abstractions;
|
||||
using WorkflowEngine.Contracts;
|
||||
using WorkflowEngine.Services;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace WorkflowEngine.Tutorials.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class WorkflowTestExamples
|
||||
{
|
||||
// ═══════════════════════════════════════════════
|
||||
// BASIC: Start workflow and verify task created
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Workflow_WhenStarted_ShouldCreateOpenTask()
|
||||
{
|
||||
// 1. Configure fake transport responses
|
||||
var transport = new RecordingSerdicaLegacyRabbitTransport()
|
||||
.Respond("pas_policy_validate", new { valid = true })
|
||||
.Respond("pas_get_policy_product_info", new
|
||||
{
|
||||
productCode = "4704",
|
||||
lob = "MOT",
|
||||
contractType = "STANDARD",
|
||||
});
|
||||
|
||||
// 2. Build test service provider
|
||||
using var provider = TechnicalStyleWorkflowTestHelpers.CreateServiceProvider(
|
||||
transport,
|
||||
WorkflowRuntimeProviderNames.SerdicaEngine);
|
||||
var runtimeService = provider.GetRequiredService<WorkflowRuntimeService>();
|
||||
|
||||
// 3. Start the workflow
|
||||
var start = await runtimeService.StartWorkflowAsync(new StartWorkflowRequest
|
||||
{
|
||||
WorkflowName = "ApproveApplication",
|
||||
Payload = new Dictionary<string, object?>
|
||||
{
|
||||
["srPolicyId"] = 12345L,
|
||||
["srAnnexId"] = 67890L,
|
||||
["srCustId"] = 11111L,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Assert task was created
|
||||
var tasks = await runtimeService.GetTasksAsync(new WorkflowTasksGetRequest
|
||||
{
|
||||
WorkflowInstanceId = start.WorkflowInstanceId,
|
||||
Status = "Open",
|
||||
});
|
||||
tasks.Tasks.Should().ContainSingle();
|
||||
tasks.Tasks.Single().TaskName.Should().Be("Approve Application");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// VERIFY: Service calls made in order
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Workflow_WhenStarted_ShouldCallServicesInOrder()
|
||||
{
|
||||
var transport = new RecordingSerdicaLegacyRabbitTransport()
|
||||
.Respond("pas_annexprocessing_alterpolicy", new
|
||||
{
|
||||
srPolicyId = 1L,
|
||||
srAnnexId = 2L,
|
||||
previouslyOpened = false,
|
||||
})
|
||||
.Respond("pas_polclmparticipants_create", new { ok = true })
|
||||
.Respond("pas_premium_calculate_for_object", new { ok = true },
|
||||
SerdicaLegacyRabbitMode.MicroserviceConsumer)
|
||||
.Respond("pas_polannexes_get", new
|
||||
{
|
||||
shortDescription = "Test annex",
|
||||
policyNo = "POL-001",
|
||||
});
|
||||
|
||||
using var provider = TechnicalStyleWorkflowTestHelpers.CreateServiceProvider(
|
||||
transport,
|
||||
WorkflowRuntimeProviderNames.SerdicaEngine);
|
||||
var runtimeService = provider.GetRequiredService<WorkflowRuntimeService>();
|
||||
|
||||
await runtimeService.StartWorkflowAsync(new StartWorkflowRequest
|
||||
{
|
||||
WorkflowName = "AssistantAddAnnex",
|
||||
Payload = new Dictionary<string, object?>
|
||||
{
|
||||
["srPolicyId"] = 1L,
|
||||
["srAnnexId"] = 2L,
|
||||
["policyExistsOnIPAL"] = true,
|
||||
["annexPreviouslyOpened"] = false,
|
||||
["annexType"] = "BENEF",
|
||||
["entityData"] = new { srCustId = 3L },
|
||||
},
|
||||
});
|
||||
|
||||
// Verify exact call sequence
|
||||
transport.Invocations.Select(x => x.Command)
|
||||
.Should().Equal(
|
||||
"pas_annexprocessing_alterpolicy",
|
||||
"pas_polclmparticipants_create",
|
||||
"pas_premium_calculate_for_object",
|
||||
"pas_polannexes_get");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TASK COMPLETION: Approve/reject flows
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public async Task Workflow_WhenTaskCompleted_ShouldExecuteOnCompleteFlow()
|
||||
{
|
||||
var transport = new RecordingSerdicaLegacyRabbitTransport()
|
||||
.Respond("pas_operations_perform", new { passed = true, nextStep = "CONTINUE" },
|
||||
SerdicaLegacyRabbitMode.MicroserviceConsumer)
|
||||
.Respond("pas_polreg_convertapltopol", new { ok = true })
|
||||
.Respond("pas_annexprocessing_generatepolicyno", new { ok = true })
|
||||
.Respond("pas_get_policy_product_info", new
|
||||
{
|
||||
productCode = "4704",
|
||||
lob = "MOT",
|
||||
contractType = "STANDARD",
|
||||
});
|
||||
|
||||
using var provider = TechnicalStyleWorkflowTestHelpers.CreateServiceProvider(
|
||||
transport,
|
||||
WorkflowRuntimeProviderNames.SerdicaEngine);
|
||||
var runtimeService = provider.GetRequiredService<WorkflowRuntimeService>();
|
||||
|
||||
// Start workflow
|
||||
var start = await runtimeService.StartWorkflowAsync(new StartWorkflowRequest
|
||||
{
|
||||
WorkflowName = "ApproveApplication",
|
||||
Payload = new Dictionary<string, object?>
|
||||
{
|
||||
["srPolicyId"] = 1L,
|
||||
["srAnnexId"] = 2L,
|
||||
["srCustId"] = 3L,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the open task
|
||||
var task = (await runtimeService.GetTasksAsync(new WorkflowTasksGetRequest
|
||||
{
|
||||
WorkflowInstanceId = start.WorkflowInstanceId,
|
||||
Status = "Open",
|
||||
})).Tasks.Single();
|
||||
|
||||
// Complete with "approve"
|
||||
await runtimeService.CompleteTaskAsync(new WorkflowTaskCompleteRequest
|
||||
{
|
||||
WorkflowTaskId = task.WorkflowTaskId,
|
||||
ActorId = "test-user",
|
||||
ActorRoles = ["DBA"],
|
||||
Payload = new Dictionary<string, object?> { ["answer"] = "approve" },
|
||||
});
|
||||
|
||||
// Verify operations and conversion were called
|
||||
transport.Invocations.Should().Contain(x => x.Command == "pas_operations_perform");
|
||||
transport.Invocations.Should().Contain(x => x.Command == "pas_polreg_convertapltopol");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// RECORDING TRANSPORT: multiple responses
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Test]
|
||||
public void RecordingTransport_CanConfigureMultipleResponses()
|
||||
{
|
||||
var transport = new RecordingSerdicaLegacyRabbitTransport()
|
||||
// Default mode (Envelope)
|
||||
.Respond("command_a", new { result = "first" })
|
||||
// Specific mode
|
||||
.Respond("command_b", new { result = "second" },
|
||||
SerdicaLegacyRabbitMode.MicroserviceConsumer)
|
||||
// Same command, different responses (returned in order)
|
||||
.Respond("command_c", new { attempt = 1 })
|
||||
.Respond("command_c", new { attempt = 2 });
|
||||
|
||||
// After workflow execution, inspect:
|
||||
// transport.Invocations — list of all calls made
|
||||
// transport.Invocations[0].Command — command name
|
||||
// transport.Invocations[0].Payload — request payload
|
||||
transport.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user