Introduction
If you are like most developers these days, then you are probably writing at least some unit tests for your projects, and if you are writing unit tests then you are likely familiar with the concept of using stubs, fakes, and mocks to stand in for code that has not been written yet or to isolate the subject under test (SUT) from its depended on components (DOCs).
There are many mature and robust mocking frameworks available like FakeItEasy, JustMock Lite, Moq, NSubstitute, and Rhino Mocks that you can employ to stand in for your DOCs. If you aren’t familiar with at least one of these frameworks, then I highly recommend taking the time; the learning curve with most of these frameworks is relatively short. I am going to assume, however, you have a reason for deciding to roll your own, and there are some good and valid reasons for doing so.
I am also going to assume you want to avoid getting bogged down into the differences between the various types of test doubles, so I am not going to discuss that here. Instead, I am just going to get on with the more pragmatic aspects and show you how to easily write configurable test doubles that can be used for both state and behavior verification as well as stand in for code that you haven’t gotten around to writing yet.
Using the Code
For a demo project, I chose a hypothetical order fulfillment application. The Order
class, which will serve as our SUT is dependent on a repository that implements the interface IInventoryRepository
to mediate between the domain and the datastore. In order to test our SUT in isolation, we will author a configurable test double to stand in for a real repository.
The Order
class looks like this:
public class Order
{
private List<LineItem> lineItems;
private IInventoryRepository inventory;
public Order(IInventoryRepository inventory)
{
this.lineItems = new List<LineItem>();
this.inventory = inventory;
}
public bool IsCompleted { get; private set; }
public void AddLineItem(int sku, int count)
{
var lineItem = this.lineItems.FirstOrDefault(li => li.SKU == sku);
var product = this.inventory[sku];
if (product == null) throw new
InvalidOperationException("Product does not exist.");
if (lineItem != null)
{
if(count + lineItem.Count > product.Available) throw new
InvalidOperationException("Insufficient quantity available.");
this.lineItems.Remove(lineItem);
lineItem.Count += count;
}
else
{
if (count > product.Available) throw new
InvalidOperationException("Insufficient quantity available.");
lineItem = new LineItem() { SKU = sku, Count = count };
}
this.lineItems.Add(lineItem);
}
public void Complete()
{
var products = new List<Product>();
this.lineItems.ForEach(li =>
{
var product = this.inventory[li.SKU];
product.Decrement(li.Count);
products.Add(product);
});
this.inventory.Save(products.ToArray());
IsCompleted = true;
}
private class LineItem
{
public int SKU { get; set; }
public int Count { get; set; }
}
}
The interface IInventoryRepository
is very simple with only an indexer property to retrieve products and a Save
method that is used to update inventory quantities when the order is completed:
public interface IInventoryRepository
{
Product this[int sku] { get; }
void Save(params Product[] products);
}
So, let's get started!
Rolling your own test double is fairly straightforward. We could for most tests get away with rolling something static. The primary issue with static test doubles though is they are, well, static. If you wanted to write a set of tests to verify the SUT can respond appropriately to receiving both valid and invalid or unexpected inputs from a DOC, you would need to author separate sets of test doubles to provide those inputs; one set for valid inputs and another set for invalid inputs. For example, what if you wanted to make the Save
method or indexer property throw some file I/O exception - something that could happen if the DOC were a real repository - to make sure the SUT handles it correctly? More test doubles mean more code, and more code - even test code - means more code to maintain. That is not what we want.
What we want are configurable test doubles. Configurable doubles will let us set expectations for each test without the need to author a new double. The way we will do this is by setting up delegates for each member that may get called during a test:
class TestInventoryRepository : IInventoryRepository
{
public TestInventoryRepository() { }
public Action<Product[]> SaveAction { get; set; }
public Func<int, Product> IndexerFunction { get; set; }
public Product this[int sku]
{
get
{
return IndexerFunction(sku);
}
}
public void Save(params Product[] products)
{
SaveAction(products);
}
}
The types Action
and Func
are delegates. Action
types are void
delegates, and Func
types return a value. For example, Action<Product[]>
is a delegate for a method that takes an instance of a Product
array as an argument and returns void
. Func<int, Product>
is a delegate that takes an int
as an argument and returns an instance of Product
.
In our unit tests, we can then set the expectations by assigning methods or anonymous methods to the delegates. The following configuration rigs our test double to capture the array of products the SUT passed in to the double’s Save
method so we can verify the SUT is completing the order correctly:
var savedProducts = new List<Product>();
testRepository.SaveAction = arr =>
{
savedProducts.AddRange(arr);
};
And if we want to verify the SUT can handle the DOC throwing an exception, we just assign the SaveAction
delegate the following:
testRepository.SaveAction = arr =>
{
throw new System.IO.IOException("Something bad happened.");
};
The IndexerFunction
delegate can be assigned similar code to throw an exception or to return a value:
testRepository.IndexerFunction = sku =>
{
return new Product() { SKU = sku, Description = "Test Product", Count = 100 };
};
If you want to learn more about test doubles and test patterns, I highly recommend Meszaros’s book xUnit Test Patterns: Refactoring Test Code, which is the definitive guide on the matter.
If you found this tip helpful, please feel free to leave me comments. Happy testing!