Reduce coupling: Free your code and your tests
March 29, 2016 · 6 minute read
If you’ve ever tried to refactor some code only to find that your tests broke (even though you retained the same functionality) then the chances are your tests were tightly coupled to your implementation. This coupling meant that your tests had too much knowledge of the implementation details of your code.
By reducing this coupling you can refactor your code freely in the knowledge that your tests will only fail when you’ve actually broken something, not just because you decided to apply a different pattern.
Back to the Kata
In recent posts we’ve looked at how tight coupling between parts of your code can lead to bugs and make the code harder to maintain.
We looked at an example using Tell Don’t Ask and how to use delegate callbacks in place of exceptions to reduce the problems which arise from one part of your codebase having intimate knowledge about the implementation of another.
In exploring multiple ways to tackle the shopping cart kata we came to this implementation.
public class ShoppingCart
{
private IReceipt _receipt;
private Dictionary<string, ItemDetails> _products = new Dictionary<string, ItemDetails>();
private int _total;
public ShoppingCart(IReceipt receipt)
{
_products["apple"] = new ItemDetails("apple", 10);
_products["pear"] = new ItemDetails("apple", 20);
_receipt = receipt;
}
public void Scan(string item, Action itemNotFound)
{
if (!_products.ContainsKey(item))
{
itemNotFound?.Invoke();
}
else
{
_total += _products[item].Price;
_receipt?.Add(_products[item]);
}
}
}
Now let’s say we want to test this code, specifically to check that the correct items have been scanned (and added to the receipt).
[TestFixture]
public class ShoppingCartTests
{
[Test]
public void WhenItemScannedItIsAddedToTheReceipt()
{
var simpleReceipt = new SimpleReceipt();
var subject = new ShoppingCart(simpleReceipt);
subject.Scan("apple", () => { });
Assert.That(simpleReceipt.ToString, Is.EqualTo("apple-10"+Environment.NewLine));
}
}
In this case the SimpleReceipt
has an overloaded ToString()
method which returns a very basic list of the items which have been scanned.
Incidentally, in our bid to abide by “Tell Don’t Ask” we haven’t directly exposed a list of items from our SimpleReceipt
implementation.
Identify the worst coupling and fix that first
Reflecting on this implementation we can identify the coupling that’s been introduced and use that to guide what to refactor first.
Connascence provides a taxonomy for different types of coupling and their relative strength. This gives us a tool to establish where our efforts are best directed when refactoring the code.
The different types are.
- Name: When multiple components must agree on the name of an entity
- Type: When multiple components must agree on the type of an entity
- Meaning: When multiple components must agree on the meaning of values
- Position: When multiple entities must agree on the order of values
- Algorithm: When multiple components must agree on a particular algorithm
- Execution Order: When the order of execution of multiple components is important
- Timing: When the timing of the execution of multiple components is important
- Value: When several values must change together
- Identity: When multiple components must reference the same entity
The further down the list you go the stronger and therefore more problematic the connascence is.
Connascence of Value
We’ve directly referenced the name of one of our item (“apple”) and it’s price (10) in both the production and test code. Should any of these values change (in test or production code) then our test will fail. This is an example of connascence of value, the second most serious type of connascence we can have.
So what can we do to fix it? We know that reducing the distance between the coupled items reduces the strength of the connascence. Therefore if we pull the list of items up to the test and inject it into our shopping cart we can bring the connascence into the same (test) method.
[TestFixture]
public class ShoppingCartTests
{
[Test]
public void WhenItemScannedItIsAddedToTheReceipt()
{
var simpleReceipt = new SimpleReceipt();
var productCatalog = new Dictionary<string, ItemDetails>();
productCatalog["apple"] = new ItemDetails("apple", 10);
productCatalog["pear"] = new ItemDetails("apple", 20);
var subject = new ShoppingCart(productCatalog, simpleReceipt);
subject.Scan("apple", () => { });
Assert.That(simpleReceipt.ToString, Is.EqualTo("apple-10"+Environment.NewLine));
}
}
This feels a bit better. It’s now clear when reasoning about this test where the values are coming from. In doing this we have also started to model an important domain concept, that of the product catalog which we can assume may eventually come from another source (perhaps a database).
Rinse, repeat
With that sorted we can look for the next worst example of coupling.
It’s clear that there is knowledge in the test about the implementation of our simple receipt. Our test knows to expect an item line to be printed in the form product-price+Environment.NewLine
.
This represents connascence of algorithm as both our test and SimpleReceipt
must agree on the algorithm for printing out strings.
If we stop and think about what we’re testing here, it’s worth noting that the main reason for SimpleReceipt
’s existence is to facilitate our testing. In reality it’s more likely we would have an implementation of IReceipt
which does more, perhaps print to paper.
This being the case it feels redundant to assert that SimpleReceipt
prints items in a specific format. We’re actually more interested in verifying that the correct items (and prices) are pased to whatever receipt implementation our code is using in production.
So one option here is to use the handy (but sometimes controversial) self shunt.
[TestFixture]
public class ShoppingCartTests : IReceipt
{
private List<ItemDetails> _scannedItems = new List<ItemDetails>();
[Test]
public void WhenItemScannedItIsAddedToTheReceipt()
{
var simpleReceipt = this;
var productCatalog = new Dictionary<string, ItemDetails>();
productCatalog["apple"] = new ItemDetails("apple", 10);
productCatalog["pear"] = new ItemDetails("apple", 20);
var subject = new ShoppingCart(productCatalog, simpleReceipt);
subject.Scan("apple", () => { });
Assert.That(_scannedItems, Is.EquivalentTo(new List<ItemDetails> { new ItemDetails("apple", 10) }));
}
public void Add(ItemDetails itemDetails)
{
_scannedItems.Add(itemDetails);
}
}
We’ve abandoned SimpleReceipt
entirely at this point (based on our assumption that it only existed for testing purposes anyway) and instead injected the test itself as the receipt implementation.
This means we can easily capture the event which occurs every time an item is added to the receipt and it’s trivial to check what was added by keeping our own record.
For this to work we’d have to ensure the ItemDetails
class can be tested for equality by checking it’s values (override Equals()
, GetHashCode()
).
An alternative
Finally, an alternative is to go back to an option we considered in a previous post, not using a receipt object at all. Instead we can pass a callback delegate into our scan method.
public void Scan(string item, Action<ItemDetails> itemScanned, Action itemNotFound)
{
if (!_products.ContainsKey(item))
{
itemNotFound?.Invoke();
}
else
{
_total += _products[item].Price;
itemScanned?.Invoke(_products[item]);
}
}
Our test can now provide a handler for items being successfully scanned and test it’s own state directly.
[Test]
public void WhenItemScannedItIsAddedToTheReceipt()
{
var scannedItems = new List<ItemDetails>();
var productCatalog = new Dictionary<string, ItemDetails>();
var apple = new ItemDetails("apple", 10);
productCatalog["apple"] = apple;
var subject = new ShoppingCart(productCatalog);
subject.Scan("apple", scannedItem => scannedItems.Add(scannedItem), () => { });
Assert.That(_scannedItems, Is.EquivalentTo(new List<ItemDetails> { apple }));
}
Summary
Learning to spot coupling (connascence) when writing unit tests can help ensure your tests are not unnecessarily coupled to your production code. This has the side effect of improving the overall design of your application and can help you to write cleaner code which is less prone to bugs.