Header menu logo FCQRS

Test your domain

The valuable logic — decide and fold — is pure and has no dependency on Akka.NET, so you test it with plain function calls. No actor system, no database, no async, no facade.

You only need to wrap your command payload in the Command<_> envelope the handler expects. A couple of tiny helpers keep tests readable; Fcqrs.newCid mints the correlation id for you.

open FCQRS.Common
open FCQRS.Model.Data
open FCQRS.FSharp

let cmd details : Command<_> =
    { CommandDetails = details
      CreationDate = System.DateTime.UtcNow
      Id = None
      Sender = None
      CorrelationId = Fcqrs.newCid ()
      Metadata = Map.empty }

Test a decision

These use the Document from the tutorialCreateOrUpdate produces Updated.

let doc =
    Document.Root.TryCreate(System.Guid.NewGuid(), "Spec", "draft") |> Result.value

// a write persists Updated
let action = Document.decide (cmd (Document.CreateOrUpdate doc)) Document.initial
test <@ action = PersistEvent (Document.Updated doc) @>

For an aggregate with rejections — like the extended Document from chapter 3, which adds Approve/Errored — assert the deferred event the same way:

// approving a document that doesn't exist yet is a deferred rejection
let action2 = Document.decide (cmd Document.Approve) Document.initial
test <@ action2 = DeferEvent (Document.Errored Document.DocumentNotFound) @>

Test the fold

fold takes the Event<_> envelope; build one (or fold a list to assert the end state):

let evt details : Event<_> =
    { EventDetails = details
      CreationDate = System.DateTime.UtcNow
      Id = None
      Sender = None
      CorrelationId = Fcqrs.newCid ()
      Version = 1L |> ValueLens.TryCreate |> Result.value
      Metadata = Map.empty }

let state = Document.fold (evt (Document.Updated doc)) Document.initial
test <@ state.Document = Some doc @>
test <@ state.Version = 1L @>

In C#, decide/fold are the aggregate's HandleCommand/ApplyEvent methods, and TestEnvelope wraps the payload (pass a FakeTimeProvider to test time-dependent logic deterministically):

// No actor system, no DI — construct the aggregate and call the methods directly.
using static FCQRS.CSharp;   // TestEnvelope, EventActions
using Xunit;

var agg = new DocumentAggregate();

// a write persists Updated
var cmd = TestEnvelope.Command(new DocumentCommand.CreateOrUpdate(doc), TimeProvider.System);
var action = agg.HandleCommand(cmd, DocumentState.Initial);
Assert.Equal(EventActions.Persist<DocumentEvent>(new DocumentEvent.Updated(doc)), action);

// the fold advances the version
var evt = TestEnvelope.Event(new DocumentEvent.Updated(doc), version: 1, TimeProvider.System);
var state = agg.ApplyEvent(evt, DocumentState.Initial);
Assert.Equal(1, state.Version);

That is the payoff of keeping the write side pure (see Aggregates): your core business rules are testable in isolation, fast, and deterministic. Saga handleEvent/applySideEffects functions are pure in the same way and test the same way.

val cmd: details: 'a -> 'b
val details: 'a
namespace System
Multiple items
[<Struct>] type DateTime = new: date: DateOnly * time: TimeOnly -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>

--------------------
System.DateTime ()
   (+0 other overloads)
System.DateTime(ticks: int64) : System.DateTime
   (+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly) : System.DateTime
   (+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : System.DateTime
   (+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly, kind: System.DateTimeKind) : System.DateTime
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int) : System.DateTime
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : System.DateTime
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : System.DateTime
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : System.DateTime
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : System.DateTime
   (+0 other overloads)
property System.DateTime.UtcNow: System.DateTime with get
<summary>Gets a <see cref="T:System.DateTime" /> object that is set to the current date and time on this computer, expressed as the Coordinated Universal Time (UTC).</summary>
<returns>An object whose value is the current UTC date and time.</returns>
union case Option.None: Option<'T>
Multiple items
module Map from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...

--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
val empty<'Key,'T (requires comparison)> : Map<'Key,'T> (requires comparison)
val doc: obj
Multiple items
[<Struct>] type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: utf8Destination: Span<byte> * bytesWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>

--------------------
System.Guid ()
System.Guid(b: byte array) : System.Guid
System.Guid(b: System.ReadOnlySpan<byte>) : System.Guid
System.Guid(g: string) : System.Guid
System.Guid(b: System.ReadOnlySpan<byte>, bigEndian: bool) : System.Guid
System.Guid(a: int, b: int16, c: int16, d: byte array) : System.Guid
System.Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : System.Guid
System.Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : System.Guid
System.Guid.NewGuid() : System.Guid
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val action: obj
val action2: obj
val evt: details: 'a -> Event<'b>
Multiple items
module Event from Microsoft.FSharp.Control

--------------------
type Event<'T> = new: unit -> Event<'T> member Trigger: arg: 'T -> unit member Publish: IEvent<'T>

--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate and reference type)> = new: unit -> Event<'Delegate,'Args> member Trigger: sender: obj * args: 'Args -> unit member Publish: IEvent<'Delegate,'Args>

--------------------
new: unit -> Event<'T>

--------------------
new: unit -> Event<'Delegate,'Args>
val state: obj
union case Option.Some: Value: 'T -> Option<'T>

Type something to start searching.