Testing with .NET
TinyConductor.Testing runs an Embedded engine inside your test process: no
server, no port, no container. It loads the engine’s native library
(bpm-embedded-ffi), which is the Embedded server itself, so your tests run
the same engine and get the same answers as production, and gives you a typed
API. The API is synchronous: the
engine runs in your process and handles one call at a time, so async
would only add noise. The full reference is the
.NET testing SDK.
The runnable project is testing/dotnet in the
examples download.
Set up
Section titled “Set up”The SDK is planned to ship as the NuGet package TinyConductor.Testing, with
the native library for every platform inside it, so that
dotnet add package TinyConductor.Testing is the whole setup. It is not on
NuGet yet. Until it is, a release provides the two parts as files:
-
The SDK package. The release has the package file
TinyConductor.Testing.0.1.0.nupkg. Keep it in a folder and make that folder a package source for your test project, with anuget.confignext to the project (or in a folder above it):<configuration><packageSources><add key="tinyconductor-release" value="path/to/the/folder/with/the/nupkg" /></packageSources></configuration>A relative
valueis relative to thenuget.configfile. Then add the package as usual:Terminal window dotnet add package TinyConductor.Testing --version 0.1.0The package holds the SDK only, not the native library.
-
The native library. Download and unpack the release archive for your platform (the list is in Embedded mode). The library is in its
libfolder:libbpm_embedded_ffi.dylibon macOS,libbpm_embedded_ffi.soon Linux andbpm_embedded_ffi.dllon Windows. Copy it next to your test assembly; the example’s project file does that, and fails the build with a clear message if the library is missing:<None Include="$(TinyConductorNativeDir)/$(TinyConductorNativeName)"Link="$(TinyConductorNativeName)" CopyToOutputDirectory="PreserveNewest" /> -
Put the BPMN, DMN and form files in the output directory (the example copies the download’s
order.*files intoprocesses/).
The example project is set up this way already. Put the downloads in one folder, the examples download and the release archive unpacked and the package file as it is:
downloads/ tinyconductor-0.1.0-examples/ tinyconductor-0.1.0-linux-x86_64/ TinyConductor.Testing.0.1.0.nupkgIts nuget.config makes the downloads folder the package source for
TinyConductor.Testing (the test framework comes from nuget.org), and the
project takes the native library from the release archive for your platform
in that folder. If the archive is somewhere else, point
TinyConductorNativeDir at its lib folder:
dotnet test -p:TinyConductorNativeDir=/path/to/tinyconductor-0.1.0-linux-x86_64/lib.
The test
Section titled “The test”using TinyConductor.Testing;using Xunit;
public sealed class OrderProcessTests : IDisposable{ // xUnit creates a new instance, and so a new engine, for every test. private readonly EmbeddedEngine _engine = new();
[Fact] public void Order_completes_after_payment_review_period_and_confirmation() { _engine.DeployResource("processes/order.form"); _engine.DeployResource("processes/order-decisions.dmn"); _engine.DeployResource("processes/order.bpmn"); _engine.MockJobWorker("send-invoice").ThenComplete();
var order = _engine.CreateInstance( "order-process", new { orderId = 4711, amount = 99.90 });
_engine.CompleteJob("charge-card", new { charged = true }); // The 24-hour review timer fires inside this call; no sleeping or polling. _engine.Clock.AdvanceBy(TimeSpan.FromDays(1)); Assert.Single(_engine.UserTasks).Complete(new { shipped = true });
TinyConductorAssert.That(order) .IsCompleted() .HasCompletedElements( "charge-card", "shipping-tier", "review-period", "send-invoice", "confirm-shipment") .HasVariable("charged", true) .HasVariable("shippingTier", "standard") .HasVariable("shipped", true) .HasNoIncidents(); }
public void Dispose() => _engine.Dispose();}Run it:
cd tinyconductor-0.1.0-examples/testing/dotnetdotnet testThe API in brief
Section titled “The API in brief”| Need | Call |
|---|---|
| deploy a resource | engine.DeployResource(path), engine.DeployResourceXml(name, xml) |
| start an instance | engine.CreateInstance(processId, variables) |
| act as a worker | engine.CompleteJob(type, variables), engine.ActivateJobs(type) then job.Complete(), job.Fail(...), job.ThrowError(code) |
| let a worker answer automatically | engine.MockJobWorker(type).ThenComplete() / .ThenThrowError(...) / .ThenHandle(job => …) |
| messages and signals | engine.PublishMessage(...), engine.BroadcastSignal(...) |
| user tasks | engine.UserTasks, task.Assign(...), task.Complete(...) |
| time | engine.Clock.AdvanceBy(TimeSpan), engine.Clock.AdvanceTo(DateTimeOffset), engine.Clock.Current |
| assertions | TinyConductorAssert.That(instance): IsCompleted, IsActive, IsTerminated, HasCompletedElements, HasActiveElements, HasVariable, HasVariableNames, HasNoIncidents, HasActiveIncidents |
| start over | engine.Reset(); disposing the engine drops everything it held |
Moving the clock returns only after everything it caused has run: the
timers due at the new instant, the engine work they trigger and any
registered worker mocks. An assertion straight after AdvanceBy therefore
sees the result.
Jobs have leases, as on a server: ActivateJobs leases each job for 30
seconds of engine time. If you move the clock past that before completing or
failing the job, the job is offered again, and completing the old
ActivatedJob fails with EmbeddedEngineException. Activate it again after
the clock move.
Variables may be anonymous objects, POCOs or dictionaries. Read a job’s
variables with job.GetVariable<T>("name"). For a JSON operation the typed
API does not cover yet, engine.Transport.Send(json) sends it raw (see
C and FFI for the operations).