Unit testing private methods

I’m starting a new series about unit testing anti-patterns. This post is the first article in that series.

When it comes to unit testing, one of the most commonly asked questions is: how to test a private method?

Unit testing private methods

I think one of the biggest misconceptions in unit testing is this notion that, when you test a class, you should cover each and every method in it with unit tests. A logical extension of this thought is to try to cover private methods with tests too. Just make them public - not a big deal! - and be done with it.

Well, that’s a horrible approach to unit testing. By exposing methods that you would otherwise keep private, you reveal implementation details. And by coupling tests to those implementation details, you get a fragile unit test suite.

When your tests start to know too much about the internals of the system under test (SUT), that leads to false positives during refactoring. Which means they won’t act as a safety net anymore and instead will impede your refactoring efforts because of the necessity to refactor them along with the implementation details they are bound to. Basically, they will stop fulfilling their main objective: providing you with the confidence in code correctness.

When it comes to unit testing, you need to follow this one rule: test only the public API of the SUT, don’t expose its implementation details in order to enable unit testing. Your tests should use the SUT the same way its regular clients do, don’t give them any special privileges. Here you can read more about what an implementation detail is and how it is different from public API: link.

The answer to the question of how to test a private method is then: nohow. Just don’t do that. Let the SUT sort its behavior out the way it wants, only test the observable results of that behavior. The behavior the SUT’s clients can observe should be the one - and the only! - target for verification in tests.

But what if the private method is too complex and leaving it untested is too dangerous? What if there’s too much logic in some private method and unit testing it through the SUT’s public API is not feasible?  That’s an indicator that you miss an abstraction here. Instead of making this method public, extract its inner workings into a separate class and test that class.

Look at the following example:

public class Order
{
    private Customer _customer;
    private List<Product> _products;

    public string GenerateDescription()
    {
        return $"Customer name: {_customer.Name}, " +
            $"total number of products: {_products.Count}, " +
            $"total price: {GetPrice()}";
    }

    private decimal GetPrice()
    {
        decimal basePrice = /* Calculate based on _products */;
        decimal discounts = /* Calculate based on _customer */;
        decimal taxes = /* Calculate based on _products */;
        return basePrice - discounts + taxes;
    }
}

Here, the order’s GenerateDescription() itself is quite simple: it just returns some generic description of the order. But it uses the private GetPrice() which is much more complex: it contains an important business logic which needs to be thoroughly tested. This complexity is a strong sign of a hidden abstraction.

Instead of making this method public, you can introduce a separate domain concept, PriceCalculator:

public class Order
{
    private Customer _customer;
    private List<Product> _products;

    public string GenerateDescription()
    {
        var calculator = new PriceCalculator();

        return $"Customer name: {_customer.Name}, " +
            $"total number of products: {_products.Count}, " +
            $"total price: {calculator.Calculate(_customer, _products)}";
    }
}

public class PriceCalculator
{
    public decimal Calculate(Customer customer, List<Product> products)
    {
        decimal basePrice = /* Calculate based on products */;
        decimal discounts = /* Calculate based on customer */;
        decimal taxes = /* Calculate based on products */;
        return basePrice - discounts + taxes;
    }
}

This new class now can be tested separately from Order. And you can also use the functional style of unit testing here because this class itself doesn’t maintain any internal state: it generates the output based on the provided input.

Unit testing internal classes

There’s another related problem that often arises along with the problem of unit testing private methods. And that is: how to test internal classes? The situation with them is not as straightforward as with private methods.

Let’s extend our example with the Order and PriceCalculator classes and say that we want to make PriceCalculator internal because it’s not used anywhere outside the domain model. Which is all located in the single assembly.

That’s a reasonable decision. It’s preferable to keep the domain model’s API surface as small as possible and not expand it without necessity.

We could use the InternalsVisibleTo attribute on the domain assembly and make the internal classes visible to the unit tests. But wouldn’t it entail the same problem we had with private methods? In other words, wouldn’t we couple the tests to internal implementation details and make them fragile? After all, there’s no client outside the domain model that uses PriceCalculator, so why would we provide the unit tests with the special privilege like that?

That’s a fair question. And the answer is: no, we wouldn’t be coupling the tests to implementation details.

To see why let’s take a look at the two requirements I brought up earlier:

  • Unit tests should use only the SUT’s public API.

  • Unit tests should mimic the behavior of the SUT’s clients.

While it’s true that PriceCalculator itself is internal, it still has a public API and clients that use that API. It’s just both the calculator and its clients are located in the same assembly.

We can depict this situation as follows:

Unit testing an internal class
Unit testing an internal class

The domain model here is represented as a green circle. While there are no external clients using it, the domain model itself consists of multiple layers and members of outer layers utilize the inner layers in order to achieve their goals. They act as clients in relation to members of those inner layers.

So in order to unit test a member of the inner layer, we can use the same public API used by the clients from the domain model. In our case with Order and PriceCalculator, we can write tests against PriceCalculator.Calculate() because that’s the API the Order class invokes.

The fact that PriceCalculator and the unit tests reside in separate assemblies is just a technical inconvenience here. It shouldn’t stop us from unit testing this class. Despite being internal, it still has a public API which we can bind to in tests.

And I personally don’t even make those classes internal anymore, although they don’t have "normal" clients outside the same assembly. It simplifies unit testing at the expense of widening the domain model’s API surface but I find this trade-off worth making.

Summary

  • Don’t aim at unit testing each method in the SUT. Unit test only the publicly available API.

  • When writing unit tests, mimic the behavior of the SUT’s clients.

  • Don’t test private methods. Either unit test them indirectly, using the public API, or extract them into separate classes and test those classes instead.

  • Don’t hesitate to unit test internal classes as long as you follow the guidelines above.

If you enjoy this article, check out my Pragmatic Unit Testing training course, you will love it too.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus