Skip to main content

Command Palette

Search for a command to run...

Testability of Large Objects

Updated
3 min read

Okay I lied

“Testability of functions which need to accept large objects as a parameter but only use a fraction of the properties of the object” just do not roll off the tongue.

We’re going to take a look at some strategies for increasing the testability of your code. Particularly when the function you’re testing requires you to mock large input objects. This blog will not discuss strategies for impure functions that require large, mocked services, as Mark Seemann has already described how to structure an impure function to extract the testable pure parts of it.

Just pass the parameters you need

// Instead of:
public static int SomeFunction(SomeLargeObject obj)
{
  return obj.SomeProp + obj.SomeOtherProp
}

// Try:
public static int SomeFunction(int someProp, int SomeOtherProp)
{
  return somePromp + someOtherProp
}

If SomeLargeObject has several required properties and complicated rules to enforce invariants, then testing SomeFunction becomes much easier if we just pass in the information it needs.

Sometimes the number of parameters gets quick long. We can pass in an object which has only these properties, but copying all the fields from the old object to the new object creates some verbose code, and it’s particularly dangerous if the function has multiple call sites.

Ideally, in our domain code we’d like to pass in our whole domain object for simplicity, but in our test code pass in just the properties the function actually needs to return a result.

The solution is something I’m calling a facet, as in “a particular aspect or feature of something”. First, we make an object that will serve as our parameter to our function, containing only the properties it needs. Then, using the features of C# we create an implicit converter from our domain object to our facet. This is what will allow us to pass in either the facet, or the domain model.

public class Account
{
 // Incredibly large domain model omitted for sanity
}

// This object contains the information needed
// to calculate loyalty, the age of the account
// and total amount spent
public record AccountLoyaltyInformation(
  DateTime CreationDate,
  decimal TotalSpent);
{   
  // create an implicit conversion from our domain model
  public static implicit operator AccountLoyaltyInformation(Account account)
    => new AccountLoyaltyInformation(account.CreationDate, account.TotalSpent);
}

public int CalculateLoyalty(AccountLoyaltyInformation loyaltyInfo)
{
  // Implementation omitted
}

// Use the parameter object directly to simplify testing
public void TestLoyaltyCalculations(Date someDate, decimal someTotalSpent, int expectedLoyaltyRating)
{
  int actualLoyaltyRating = CalculateLoyalty(new(someDate, someTotalSpent));
  actualLoyaltyRating.ShouldBe(expectedLoyaltyRating);
}

// pass in the domain object directly in the application code
public async Task<int> GetCustomerLoyalty(Guid customerId)
{
  Account account = await _db.LoadCustomerAccount(customerId);
  return CalculateLoyalty(account); // implicitly converts to 'AccountLoyaltyInformation'
}

Why not an object property?

At first it might seem like the obvious better solution is to simply make some LoyaltyInformation object a property on the Account object, and we can get rid of all this nonsense around implicit conversions.

But often, Account is a large object with a rich history of why it’s properties are subdivided the way they are, and some new feature may just have to deal with the fact that CreationDate is on Account.AuditInformation and TotalSpent is on Account.PurchaseHistory.