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 tutorial — CreateOrUpdate
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):
|
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.
[<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)
<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>
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>
[<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
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
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>
FCQRS