Exposing private state to enable unit testing
This article is part of the series of posts about unit testing anti-patterns.
Exposing private state
Last time, we talked about making private methods public in order to enable unit testing. It’s not the only way people expose implementation details to the outside world for unit testing purposes, though. Today, we’ll look at a similar anti-pattern: exposing private state.
Let’s look at the following example:
public class Customer
{
private CustomerStatus _status = CustomerStatus.Regular;
public void Promote()
{
_status = CustomerStatus.Preferred;
}
public decimal GetDiscount()
{
return _status == CustomerStatus.Preferred ? 0.05m : 0m;
}
}
public enum CustomerStatus
{
Regular,
Preferred
}
Here, a customer starts with the Regular status and then can be promoted to Preferred and enjoy the 5% discount on everything.
How would you approach unit testing the Promote()
method in this case? The status field is private and thus is not available in tests, so there’s no direct way to verify the promotion.
A tempting solution is to just make the status field public and examine it after calling Promote()
. After all, that’s the desirable side effect of this method, so why not just test that side effect directly?
That would be an example of this anti-pattern in action. Remember: always write unit tests as if they were a regular client of the SUT, don’t expose state getters solely for satisfying a test. In the above example, the status field is not observable from the outside world and thus doesn’t belong to the class’s public API. Exposing it means that you would expose an internal implementation detail.
How to do that properly then?
What you should do instead is you need to mimic the behavior of regular clients of this class. In this particular situation, it seems that they don’t care about the status field, otherwise that piece of information would be public. What they do care about is the discount the customer gets after the promotion.
So that is what you should aim at verifying too. You need to check two things here:
-
A newly created customer has no discount.
-
Once the customer is promoted, the discount becomes 5%.
This way, you are binding your tests to the observable behavior of the SUT, not its implementation details. Later, if your application code starts to use this status field (say, for reporting purposes), you will be able to also start binding to it in tests because it’d officially become part of the SUT’s public API. But not until that happens. Widening the SUT’s API surface for the sake of testability is not a good design decision.
Testing a set of collaborating classes
Additional state getters are also often introduced when verifying collaborations between classes. In order to check the effect of one class calling another, you could decide to make some piece of information on the receiving part public.
Let’s take this example:
public class Order
{
public void CheckOut(OrderStatistics orderStatistics)
{
/* other work */
decimal totalAmount = /* calculate */;
orderStatistics.LogOrder(_customer.Id, totalAmount);
}
}
public class OrderStatistics
{
private List<LogEntry> _log;
public void LogOrder(int customerId, decimal amount)
{
_log.Add(new LogEntry(customerId, amount));
}
}
Here, aside from other work related to the checkout process, the Order
class needs to record the fact of purchase using another class - OrderStatistics
. But how to verify it? Note that the _log
field is private, so there’s no direct way for us to do that.
Again, a tempting solution in this situation would be to just make the collection of log entries public and bind to it in tests.
Another possible way to fix this problem is to mock OrderStatistics
out. You could introduce an interface for it, create a runtime wrapper using one of the mocking libraries and check that Order
calls LogOrder
.
Both of these decide decisions are suboptimal. The first one is because you would expose the internal implementation detail - the private field. And the approach with mocks - because you would bind to another implementation detail: the way the domain classes communicate with each other.
When it comes to mocking, it’s preferable to mock only collaborations between bounded contexts, not collaborations inside of one. Here I wrote more on this topic: When to use mocks. In fact, excessive mocking is often the root cause of the so-called test-induced design damage. You can read about why it is so in my review of the GOOS book.
So, what would be the solution to this one? How to verify that Order
correctly logs the amount of spending?
The best way to approach this problem is to break the coupling between Order
and OrderStatistics
altogether by introducing a mediator between them.
Here’s the initial version once again:
public class Order
{
public void CheckOut(OrderStatistics orderStatistics)
{
/* other work */
decimal totalAmount = /* calculate */;
orderStatistics.LogOrder(_customer.Id, totalAmount);
}
}
public class OrderStatistics
{
private List<LogEntry> _log;
public void LogOrder(int customerId, decimal amount)
{
_log.Add(new LogEntry(customerId, amount));
}
}
Instead of calling OrderStatistics
directly, modify Order
so that its CheckOut
method returns the customer Id and the money amount. Then, use the mediator to pass those results further to OrderStatistics
:
public class CheckOutHandler
{
public void Handle()
{
Order order = /* get an order */;
OrderStatistics orderStatistics = /* get order statistics */;
(int customerId, decimal amount) = order.CheckOut();
orderStatistics.LogOrder(customerId, amount);
}
}
public class Order
{
public (int customerId, decimal amount) CheckOut()
{
/* other work */
decimal totalAmount = /* calculate */;
return (_customer.Id, totalAmount);
}
}
public class OrderStatistics
{
private List<LogEntry> _log;
public void LogOrder(int customerId, decimal amount)
{
_log.Add(new LogEntry(customerId, amount));
}
}
This way, the problem of unit testing the Order
class vanishes. You just verify the result of the operation (along with other side effects of the checkout process), and that’s it. No need in complicated mock machinery or exposing the private state.
That’s by the way generally a good approach to programming. Try to implement any feature in such a functional manner: as a sequence of operations. It would greatly simplify the code itself, as well as the unit tests covering it.
Summary
-
Exposing private state in order to enable unit testing is an anti-pattern.
-
Instead of exposing private state, mimic the regular clients' behavior in tests and bind the SUT’s public API.
-
In case of collaborating classes, don’t expose private information either. Break the coupling apart and verify each class independently.
If you enjoy this article, check out my Pragmatic Unit Testing training course, you will love it too.
Other articles in the series
-
Exposing private state to enable unit testing (this post)
- ← Unit testing private methods
- New course: Refactoring from Anemic Domain Model Towards a Rich One →
Subscribe
Comments
comments powered by Disqus