Contracts of Fake Objects can be described by properties.

The first time I tried my hand with the CQRS Booking kata, I abandoned it after 45 minutes because I found that I had little to learn from it. After all, I've already done umpteen variations of (restaurant) booking code examples, in several programming languages. The code example that accompanies my book Code That Fits in Your Head is only the largest and most complete of those.

I also wrote an MSDN Magazine article in 2011 about CQRS, so I think I have that angle covered as well.

Still, while at first glance the kata seemed to have little to offer me, I've found myself coming back to it a few times. It does enable me to focus on something else than the 'production code'. In fact, it turns out that even if (or perhaps particularly when) you use test-driven development (TDD), there's precious little production code. Let's get that out of the way first.

Production code #

The few times I've now done the kata, there's almost no 'production code'. The implied CommandService has two lines of effective code:

public sealed class CommandService
{
    private readonly IWriteRegistry writeRegistry;
    private readonly IReadRegistry readRegistry;
 
    public CommandService(IWriteRegistry writeRegistry, IReadRegistry readRegistry)
    {
        this.writeRegistry = writeRegistry;
        this.readRegistry = readRegistry;
    }
 
    public void BookARoom(Booking booking)
    {
        writeRegistry.Save(booking);
        readRegistry.RoomBooked(booking);
    }
}

The QueryService class isn't much more exciting:

public sealed class QueryService
{
    private readonly IReadRegistry readRegistry;
 
    public QueryService(IReadRegistry readRegistry)
    {
        this.readRegistry = readRegistry;
    }
 
    public static IReadOnlyCollection<Room> Reserve(
        Booking booking,
        IReadOnlyCollection<Room> existingView)
    {
        return existingView.Where(r => r.Name != booking.RoomName).ToList();
    }
 
    public IReadOnlyCollection<Room> GetFreeRooms(DateOnly arrival, DateOnly departure)
    {
        return readRegistry.GetFreeRooms(arrival, departure);
    }
}

The kata only suggests the GetFreeRooms method, which is only a single line. The only reason the Reserve function also exists is to pull a bit of testable logic back from the below Fake object. I'll return to that shortly.

I've also done the exercise in F#, essentially porting the C# implementation, which only highlights how simple it all is:

module CommandService =
    let bookARoom (writeRegistry : IWriteRegistry) (readRegistry : IReadRegistry) booking =
        writeRegistry.Save booking
        readRegistry.RoomBooked booking
 
module QueryService =
    let reserve booking existingView =
        existingView |> Seq.filter (fun r -> r.Name <> booking.RoomName)
 
    let getFreeRooms (readRegistry : IReadRegistry) arrival departure =
        readRegistry.GetFreeRooms arrival departure

That's both the Command side and the Query side!

This represents my honest interpretation of the kata. Really, there's nothing to it.

The reason I still find the exercise interesting is that it explores other aspects of TDD than most katas. The most common katas require you to write a little algorithm: Bowling, Word wrap, Roman Numerals, Diamond, Tennis, etc.

The CQRS Booking kata suggests no interesting algorithm, but rather teaches some important lessons about software architecture, separation of concerns, and, if you approach it with TDD, real-world test automation. In contrast to all those algorithmic exercises, this one strongly suggests the use of Test Doubles.

Fakes #

You could attempt the kata with a dynamic 'mocking' library such as Moq or Mockito, but I haven't tried. Since Stubs and Mocks break encapsulation I favour Fake Objects instead.

Creating a Fake write registry is trivial:

internal sealed class FakeWriteRegistry : Collection<Booking>, IWriteRegistry
{
    public void Save(Booking booking)
    {
        Add(booking);
    }
}

Its counterpart, the Fake read registry, turns out to be much more involved:

internal sealed class FakeReadRegistry : IReadRegistry
{
    private readonly IReadOnlyCollection<Room> rooms;
    private readonly IDictionary<DateOnly, IReadOnlyCollection<Room>> views;
 
    public FakeReadRegistry(params Room[] rooms)
    {
        this.rooms = rooms;
        views = new Dictionary<DateOnly, IReadOnlyCollection<Room>>();
    }
 
    public IReadOnlyCollection<Room> GetFreeRooms(DateOnly arrival, DateOnly departure)
    {
        return EnumerateDates(arrival, departure)
            .Select(GetView)
            .Aggregate(rooms.AsEnumerable(), Enumerable.Intersect)
            .ToList();
    }
 
    public void RoomBooked(Booking booking)
    {
        foreach (var d in EnumerateDates(booking.Arrival, booking.Departure))
        {
            var view = GetView(d);
            var newView = QueryService.Reserve(booking, view);
            views[d] = newView;
        }
    }
 
    private static IEnumerable<DateOnly> EnumerateDates(DateOnly arrival, DateOnly departure)
    {
        var d = arrival;
        while (d < departure)
        {
            yield return d;
            d = d.AddDays(1);
        }
    }
 
    private IReadOnlyCollection<Room> GetView(DateOnly date)
    {
        if (views.TryGetValue(date, out var view))
            return view;
        else
            return rooms;
    }
}

I think I can predict the most common reaction: That's much more code than the System Under Test! Indeed. For this particular exercise, this may indicate that a 'dynamic mock' library may have been a better choice. I do, however, also think that it's an artefact of the kata description's lack of requirements.

As is evident from the restaurant sample code that accompanies Code That Fits in Your Head, once you add realistic business rules the production code grows, and the ratio of test code to production code becomes better balanced.

The size of the FakeReadRegistry class also stems from the way the .NET base class library API is designed. The GetView helper method demonstrates that it requires four lines of code to look up an entry in a dictionary but return a default value if the entry isn't found. That's a one-liner in F#:

let getView (date : DateOnly) = views |> Map.tryFind date |> Option.defaultValue rooms |> Set.ofSeq

I'll show the entire F# Fake later, but you could also play some CC golf with the C# code. That's a bit besides the point, though.

Command service design #

Why does FakeReadRegistry look like it does? It's a combination of the kata description and my prior experience with CQRS. When adopting an asynchronous message-based architecture, I would usually not implement the write side exactly like that. Notice how the CommandService class' BookARoom method seems to repeat itself:

public void BookARoom(Booking booking)
{
    writeRegistry.Save(booking);
    readRegistry.RoomBooked(booking);
}

While semantically it seems to be making two different statements, structurally they're identical. If you rename the methods, you could wrap both method calls in a single Composite. In a more typical CQRS architecture, you'd post a Command on bus:

public void BookARoom(Booking booking)
{
    bus.BookRoom(booking);
}

This makes that particular BookARoom method, and perhaps the entire CommandService class, look redundant. Why do we need it?

As presented here, we don't, but in a real application, the Command service would likely perform some pre- and post-processing. For example, if this was a web application, the Command service might instead be a Controller concerned with validating and translating HTTP- or Web-based input to a Domain Object before posting to the bus.

A realistic code base would also be asynchronous, which, on .NET, would imply the use of the async and await keywords, etc.

Read registry design #

A central point of CQRS is that you can optimise the read side for the specific tasks that it needs to perform. Instead of performing a dynamic query every time a client requests a view, you can update and persist a view. Imagine having a JSON or HTML file that the system can serve upon request.

Part of handling a Command or Event is that the system background processes update persistent views once per event.

For the particular hotel booking system, I imagine that the read registry has a set of files, blobs, documents, or denormalised database rows. When it receives notification of a booking, it'll need to remove that room from the dates of the booking.

While a booking may stretch over several days, I found it simplest to think of the storage system as subdivided into single dates, instead of ranges. Indeed, the GetFreeRooms method is a ranged query, so if you really wanted to denormalise the views, you could create a persistent view per range. This would, however, require that you precalculate and persist a view for October 2 to October 4, and another one for October 2 to October 5, and so on. The combinatorial explosion suggests that this isn't a good idea, so instead I imagine keeping a persistent view per date, and then perform a bit of on-the-fly calculation per query.

That's what FakeReadRegistry does. It also falls back to a default collection of rooms for all the dates that are yet untouched by a booking. This is, again, because I imagine that I might implement a real system like that.

You may still protest that the FakeReadRegistry duplicates production code. True, perhaps, but if this really is a concern, you could refactor it to the Template Method pattern.

Still, it's not really that complicated; it only looks that way because C# and the Dictionary API is too heavy on ceremony. The Fake looks much simpler in F#:

type FakeReadRegistry (rooms : IReadOnlyCollection<Room>) =
    let mutable views = Map.empty
 
    let enumerateDates (arrival : DateOnly) departure =
        Seq.initInfinite id
        |> Seq.map arrival.AddDays
        |> Seq.takeWhile (fun d -> d < departure)
 
    let getView (date : DateOnly) =
        views |> Map.tryFind date |> Option.defaultValue rooms |> Set.ofSeq
 
    interface IReadRegistry with
        member this.GetFreeRooms arrival departure =
            enumerateDates arrival departure
            |> Seq.map getView
            |> Seq.fold Set.intersect (Set.ofSeq rooms)
            |> Set.toList :> _
            
        member this.RoomBooked booking =
            for d in enumerateDates booking.Arrival booking.Departure do
                let newView = getView d |> QueryService.reserve booking |> Seq.toList
                views <- Map.add d newView views

This isn't just more dense than the corresponding C# code, as F# tends to be, it also has a lower cyclomatic complexity. Both the EnumerateDates and GetView C# methods have a cyclomatic complexity of 2, while their F# counterparts rate only 1.

For production code, cyclomatic complexity of 2 is fine if the code is covered by automatic tests. In test code, however, we should be wary of any branching or looping, since there are (typically) no tests of the test code.

While I am going to show some tests of that code in what follows, I do that for a different reason.

Contract #

When explaining Fake Objects to people, I've begun to use a particular phrase:

A Fake Object is a polymorphic implementation of a dependency that fulfils the contract, but lacks some of the ilities.

It's funny how you can arrive at something that strikes you as profound, only to discover that it was part of the definition all along:

"We acquire or build a very lightweight implementation of the same functionality as provided by a component on which the SUT [System Under Test] depends and instruct the SUT to use it instead of the real DOC [Depended-On Component]. This implementation need not have any of the "-ilities" that the real DOC needs to have"

A common example is a Fake Repository object that pretends to be a database, often by leveraging a built-in collection API. The above FakeWriteRegistry is as simple an example as you could have. A slightly more compelling example is the FakeUserRepository shown in another article. Such an 'in-memory database' fulfils the implied contract, because if you 'save' something in the 'database' you can later retrieve it again with a query. As long as the object remains in memory.

The ilities that such a Fake database lacks are

  • data persistence
  • thread safety
  • transaction support

and perhaps others. Such qualities are clearly required in a real production environment, but are in the way in an automated testing context. The implied contract, however, is satisfied: What you save you can later retrieve.

Now consider the IReadRegistry interface:

public interface IReadRegistry
{
    IReadOnlyCollection<Room> GetFreeRooms(DateOnly arrival, DateOnly departure);
 
    void RoomBooked(Booking booking);
}

Which contract does it imply, given what you know about the CQRS Booking kata?

I would suggest the following:

  • Precondition: arrival should be less than (or equal?) to departure.
  • Postcondition: GetFreeRooms should always return a result. Null isn't a valid return value.
  • Invariant: After calling RoomBooked, GetFreeRooms should exclude that room when queried on overlapping dates.

There may be other parts of the contract than this, but I find the third one most interesting. This is exactly what you would expect from a real system: If you reserve a room, you'd be surprised to see GetFreeRooms indicating that this room is free if queried about dates that overlap the reservation.

This is the sort of implied interaction that Stubs and Mocks break, but that FakeReadRegistry guarantees.

Properties #

There's a close relationship between contracts and properties. Once you can list preconditions, invariants, and postconditions for an object, there's a good chance that you can write code that exercises those qualities. Indeed, why not use property-based testing to do so?

I don't wish to imply that you should (normally) write tests of your test code. The following rather serves as a concretisation of the notion that a Fake Object is a Test Double that implements the 'proper' behaviour. In the following, I'll subject the FakeReadRegistry class to that exercise. To do that, I'll use CsCheck 2.14.1 with xUnit.net 2.5.3.

Before tackling the above invariant, there's a simpler invariant specific to the FakeReadRegistry class. A FakeReadRegistry object takes a collection of rooms via its constructor, so for this particular implementation, we may wish to establish the reasonable invariant that GetFreeRooms doesn't 'invent' rooms on its own:

private static Gen<Room> GenRoom =>
    from name in Gen.String
    select new Room(name);
 
[Fact]
public void GetFreeRooms()
{
    (from rooms in GenRoom.ArrayUnique
     from arrival in Gen.Date.Select(DateOnly.FromDateTime)
     from i in Gen.Int[1, 1_000]
     let departure = arrival.AddDays(i)
     select (rooms, arrival, departure))
    .Sample((roomsarrivaldeparture) =>
    {
        var sut = new FakeReadRegistry(rooms);
 
        var actual = sut.GetFreeRooms(arrival, departure);
 
        Assert.Subset(new HashSet<Room>(rooms), new HashSet<Room>(actual));
    });
}

This property asserts that the actual value returned from GetFreeRooms is a subset of the rooms used to initialise the sut. Recall that the subset relation is reflexive; i.e. a set is a subset of itself.

The same property written in F# with Hedgehog 0.13.0 and Unquote 6.1.0 may look like this:

module Gen =
    let room =
        Gen.alphaNum
        |> Gen.array (Range.linear 1 10)
        |> Gen.map (fun chars -> { Name = String chars })
    let dateOnly =
        let min = DateOnly(2000, 1, 1).DayNumber
        let max = DateOnly(2100, 1, 1).DayNumber
        Range.linear min max |> Gen.int32 |> Gen.map DateOnly.FromDayNumber
 
[<Fact>]
let GetFreeRooms () = Property.check <| property {
    let! rooms = Gen.room |> Gen.list (Range.linear 0 100)
    let! arrival = Gen.dateOnly
    let! i = Gen.int32 (Range.linear 1 1_000)
    let departure = arrival.AddDays i
    let sut = FakeReadRegistry rooms :> IReadRegistry
 
    let actual = sut.GetFreeRooms arrival departure
 
    test <@ Set.isSubset (Set.ofSeq rooms) (Set.ofSeq actual) @> }

Simpler syntax, same idea.

Likewise, we can express the contract that describes the relationship between RoomBooked and GetFreeRooms like this:

[Fact]
public void RoomBooked()
{
    (from rooms in GenRoom.ArrayUnique.Nonempty
     from arrival in Gen.Date.Select(DateOnly.FromDateTime)
     from i in Gen.Int[1, 1_000]
     let departure = arrival.AddDays(i)
     from room in Gen.OneOfConst(rooms)
     from id in Gen.Guid
     let booking = new Booking(id, room.Name, arrival, departure)
     select (rooms, booking))
    .Sample((roomsbooking) =>
    {
        var sut = new FakeReadRegistry(rooms);
 
        sut.RoomBooked(booking);
        var actual = sut.GetFreeRooms(booking.Arrival, booking.Departure);
 
        Assert.DoesNotContain(booking.RoomName, actual.Select(r => r.Name));
    });
}

or, in F#:

[<Fact>]
let RoomBooked () = Property.check <| property {
    let! rooms = Gen.room |> Gen.list (Range.linear 1 100)
    let! arrival = Gen.dateOnly
    let! i = Gen.int32 (Range.linear 1 1_000)
    let departure = arrival.AddDays i
    let! room = Gen.item rooms
    let! id = Gen.guid
    let booking = {
        ClientId = id
        RoomName = room.Name
        Arrival = arrival
        Departure = departure }
    let sut = FakeReadRegistry rooms :> IReadRegistry
 
    sut.RoomBooked booking
    let actual = sut.GetFreeRooms arrival departure
 
    test <@ not (Seq.contains room actual) @> }

In both cases, the property books a room and then proceeds to query GetFreeRooms to see which rooms are free. Since the query is exactly in the range from booking.Arrival to booking.Departure, we expect not to see the name of the booked room among the free rooms.

(As I'm writing this, I think that there may be a subtle bug in the F# property. Can you spot it?)

Conclusion #

A Fake Object isn't like other Test Doubles. While Stubs and Mocks break encapsulation, a Fake Object not only stays encapsulated, but it also fulfils the contract implied by a polymorphic API (interface or base class).

Or, put another way: When is a Fake Object the right Test Double? When you can describe the contract of the dependency.

But if you can't describe the contract of a dependency, you should seriously consider if the design is right.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 13 November 2023 17:11:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 13 November 2023 17:11:00 UTC