The ObjectGodmother pattern - Solving the test data dependency problem
For most of my career, I've been working in domain-heavy teams, meaning: one of our main tasks was to understand what the business department is doing, why they are doing it, and to build digital tools around it to facilitate their work.
Almost all the time, we were using some soft of custom-DDD flavor. To have the domain logic in a central, gatekeeping place, put it as deep into the model as possible. But this domain logic needs to be tested and tested well. After all, it's the core of the application - if you don't test the domain logic, what do you test?
(I'm not going into the E2E vs integration vs unit test discussion now. I'm focusing on unit tests, but depending on how you set up your tests, you might be able to apply the same concepts to E2E and integration tests as well.)
Setting up test data for domain tests can be a pain, especially the bigger and more complex your domain becomes. The holy trinity of ObjectBuilder pattern, ObjectMother pattern and AutoFixture (a .net package for random data generation) has been a lifesaver so many times in the past. With these, we can concentrate on the main data setup in a test and let the rest be randomly generated. Let's at first define the patterns and then look at a (simplified) real-world example.
- ObjectBuilder pattern: An object builder focuses on building/configuring a single object, enabling us to set up an object just the way we need it to be for the test, without any logic applied. The object builder is fully indifferent to what you pass into it and whether it is valid (from a domain perspective) or not.
- ObjectMother patter: The object mother puts domain logic around the object builder. Its methods represent different (high-level) use-cases that pre-configure an object builder instance with data for this respective use-case. It then returns the builder for further configuring this pre-defined use-case inside a specific test.
Problem Scenario
I'm maintaining (as much as possible) a self-hostable shopping list app. It has an aggregate root Item. An item has a list of ItemTypes and every ItemType has a list of Availabilies (for every existing store up to one availability) that reference a Store by Id, define the type's price and the Section (referenced by Id as well) where it is located inside the store. Meaning, you e.g. have an item "Marmalade" with types "Strawberry" and "Peach" - the types representing different available flavors. "Strawberry" might be available in two stores (thus, has two availabilities), whereas "Peach" in only available in one of those stores (having only one availability).
You might now already recognize that we need another aggregate root for that to work: Store. A store has a name and a list of Sections (aka aisles).
In the end, leaving us with these classes (I'll make them records without logic for brevity and added an IsDeleted flag to the Store to indicate whether it is soft-deleted or not - you'll see later why we need that):
public record Item(string Name, IReadOnlyList<ItemType> ItemTypes);
public record ItemType(string Name, IReadOnlyList<Availability> Availabilities);
public record Availability(StoreId StoreId, decimal Price, SectionId SectionId);
public record Store(StoreId Id, IReadOnlyList<Section> Sections, string Name, bool IsDeleted);
public record Section(SectionId Id, string Name);An object builder for Item could look like this (Under usage of my TestCodeGenerator, I can auto-generate these classes as they are a pain to write on your own. The DomainTestBuilderBase base class is custom and abstracts away all the underlying AutoFixture configuration):
public class ItemBuilder : DomainTestBuilderBase<Item>
{
public ItemBuilder WithName(string name)
{
FillConstructorWith(nameof(name), name);
return this;
}
public ItemBuilder WithItemTypes(IReadOnlyList<ItemType> itemTypes)
{
FillConstructorWith(nameof(itemTypes), itemTypes);
return this;
}
}(What AutoFixture does, is populate the ctor parameters with the values that you pass into the .With... methods. All other parameters will be randomly filled.)
An object mother might look like this, generating a test case where the Item has two ItemTypes at the same store:
public static class ItemMother
{
public static ItemBuilder TwoTypesAtSameStore()
{
var storeId = StoreId.New;
var av1 = new AvailabilityBuilder().WithStoreId(storeId).Create();
var av2 = new AvailabilityBuilder().WithStoreId(storeId).Create();
List<ItemType> types =
[
new ItemTypeBuilder().WithAvailabilities([av1]).Create(),
new ItemTypeBuilder().WithAvailabilities([av2]).Create()
];
return new ItemBuilder().WithItemTypes(types);
}
}Notice that it's static, so in the test code you can do var item = ItemMother.TwoTypesAtSameStore().Create(), without having to initialize a mother object.
I'm sure you can imagine what this looks like for the other classes, so I'll skip their code here for brevity.
The Gap
Cool. Removing a lot of code from the test methods. But now we have a unit test where we want to test a specific Item setup. However, for this test to work, we need a not-deleted Store object that corresponds to the IDs in the Item object. This can be achieved with the StoreBuilder. But it's laborious to do and adds a lot of code to the test that does not add anything to the understanding of the test itself. We need a valid Store object, but get noise instead:
var item = ItemMother.TwoTypesAtSameStore().Create();
var av1 = item.ItemTypes[0].Availabilities[0];
var av2 = item.ItemTypes[1].Availabilities[0];
List<Section> sections =
[
// make sure the availabilities' SectionIds exist
new SectionBuilder().WithId(av1.SectionId).Create(),
new SectionBuilder().WithId(av2.SectionId).Create()
];
// make sure the availabilities' StoreId exist & store is not deleted
var store = new StoreBuilder().WithId(av1.StoreId)
.WithSections(sections).WithIsDeleted(false).Create();We add 9 lines of code, while we only wanted a valid Store object.
This is the gap that the ObjectGodmother pattern solves. It is responsible for providing a valid foundation for an object corresponding to another one. Let's build one for our store:
public class StoreGodmother
{
private Availability[]? _availabilities;
public StoreGodmother For(params Availability[] availabilities)
{
_availabilities = availabilities;
return this;
}
public StoreBuilder GetFoundation()
{
if(_availabilities is null) return new StoreBuilder();
var storeIds = _availabilities.Select(av => av.StoreId).Distinct().ToList();
// all availabilities must be for the same store as we're only building one store
if(storeIds.Count > 1)
throw new ArgumentException("All availabilities must have the same StoreId");
var sectionIds = _availabilities.Select(av => av.SectionId).Distinct().ToList();
var sections = sectionIds.Select(sectionId =>
new SectionBuilder().WithId(sectionId).Create()).ToList();
return new StoreBuilder().WithId(storeIds[0]).WithSections(sections);
}
}You might now be tempted to call the GetFoundation method directly and turn the returned builder into a Store object:
var item = ItemMother.TwoTypesAtSameStore().Create();
var avs = item.ItemTypes.SelectMany(t => t.Availabilities).ToList();
var store = new StoreGodmother().For(avs).GetFoundation().Create();But the Godmother pattern itself does not ensure a valid object for the test! That's not what it's responsible for. If you compare the previous setup with this one, you'll notice that the Godmother does not set the WithIsDeleted(false) to ensure that the store is not marked as soft-deleted.
So, let's combine it with the ObjectMother pattern:
public static class StoreMother
{
public static StoreBuilder Active(StoreGodmother? gm = null)
{
return (gm?.GetFoundation() ?? new StoreBuilder())
.WithIsDeleted(false);
}
}var item = ItemMother.TwoTypesAtSameStore().Create();
var avs = item.ItemTypes.SelectMany(t => t.Availabilities).ToList();
var store = StoreMother.Active(new StoreGodmother().For(avs)).Create();We pass the godmother into the mother's method and let the method get the foundation. Making the parameter by default null enables us to still call it without a godmother, like var store = StoreMother.Active().Create(), in case we only need an active store and don't care about the rest.
This makes it possible to outsource the logic that is merely responsible to construct valid references to another object into the Godmother class and clean up our unit test. This might not look like much, but having worked in highly complex scenarios with huge domain models, code like this can escalate really quickly - only for ensuring valid object references that don't add any meaning to the test.
Design Considerations
While you are welcome to implement the pattern in your own way, I'd like to explain some of the considerations behind my implementation.
- Why the
.For(...)method? Why not pass theAvailabilityarray into the godmother's ctor?
While that is technically possible and should suffice in most cases, I've migrated only small parts of my code to this pattern, and I'm already seeing that a godmother needs to handle different reference types at the same time. Extending the example, I have another type of availability that sits on theItemdirectly and the godmother needs to be able to handle either one of the two types, or both at the same time and create a valid store foundation. Thus, I found it easier to add another.For(params OtherAvailability[] avs)to the godmother and chain them at will, depending on my needs. - Why's the godmother passed into the mother's method, not the ctor?
I like my mothers static in order to save the code of a ctor call because you can't chain multiple mother methods anyway (they return a builder, not a mother) and thus it makes little difference to the internal code if you store the godmother in a field or pass it into the method directly.
> Where's the name coming from?
I recently read "Nettle and Bone" by T. Kingfisher and in it, godmothers are magic-capable women who bless a child upon birth to define the foundation of its life. And I found that very fitting, needing a name for something that lays the foundation of an object's configuration before it's further configured (raised?) by a mother.
Conclusion
When it comes to domain data generation, especially in DDD-like scenarios, the ObjectGodmother pattern can help to further reduce code in unit tests by outsourcing the setup of valid object references. To again give a short overview, this is what the three patterns are responsible for:
- ObjectBuilder: Configure one object, no logical checks
- ObjectMother: Pre-configure one object for a certain use-case
- ObjectGodmother: Pre-configure one object to ensure valid references to another object