DEV Community

Mohsen Bazmi
Mohsen Bazmi

Posted on • Updated on

Don’t call constructors in unit tests

Writing good tests is tricky. Modifying implementation details should not break tests. A lot of times constructors are implementation details. Especially the ones we hide behind interfaces. Also, constructors usually change more frequently than methods. In this post, we're going to quickly see a couple of simple examples of how calling constructors in unit tests can turn into a disaster, then we will see some workarounds to avoid them in different situations.

Let's start with a simple immutable Point Value Object:

var point = new Point(x : 32, y : -9);
Enter fullscreen mode Exit fullscreen mode

And test if it remembers the x we passed to it's constructor:

public void Point_remembers_its_x()
{
    var point = new Point(x : 32, y : -9);
    point.X.Should().Be(32);
}
Enter fullscreen mode Exit fullscreen mode

Look at the magic numbers: 32 , -9 ,....
The test is not trustworthy. I would generate them randomly.The trustworthiness can be a subject for another post. I'd use the magic numbers for these series for the sake of simplicity.

Notice that for assertion the FluentAssertions library is used here. You can use any tool that's convenient for you depending on the programming language you use.
The implementation of production code is straightforward.

Now Let's add another test:

public void Point_rejects_x_when_it_is_greater_than_90()
{
    Action newPoint = () => new Point(x: 92, y: -9);
    newPoint.Should().Throw<ArgumentOutOfRangeException>();
}
Enter fullscreen mode Exit fullscreen mode

There are also other tests:

  • Point rejects X when it is smaller than -90.
  • Point rejects Y when it is greater than -180.
  • Point rejects Y when it is smaller than -180.
  • Point remembers it's Y. I skip writing the test bodies in the post as they are straightforward. So we have 6 tests so far and each one call the constructor of the Point. A couple of iterations later it turns out that Z is also required to be a part of Point. That requires shotgun surgery of all of the 6 unit tests! The Point is a fairly simple predictable object, what about more complex objects with more unit tests? By each change in a constructor we need to update tens of tests. It not only is time consuming and boring, but also every time you change a unit test you reduce it's trustworthiness. We may change the signature of a constructor, and forget to update the test accordingly. So an old test may pass Y as X parameter to the constructor somewhere. That small change ends up with unit tests that should not pass but they do. And a lot of times not having a safety net at all is much better than having an untrustworthy one.

A simple solution would be setting default parameters to avoid changing all of the tests:

public class Point
{
    public Point(double x, double y, double z = 0)
    {
...
Enter fullscreen mode Exit fullscreen mode

That works here. Changes to the constructor parameters no longer need all of the tests to be updated, but there are still other problems that we may face when unit testing less obvious domains. For example a new parameter may require to be placed between the existing parameters, and as we know many programming languages (like C#) require optional parameters to be last parameters.

Another solution would be extracting the creation of a valid instance of the class to a factory method, so that every time the constructor changes, a single factory method needs to be changed.

public Point CreateAPoint()
{
    return new Point(32, -9);
}
Enter fullscreen mode Exit fullscreen mode

So all tests call this single factory method. If the constructor changes later on, I will easily change this single factory method to support all of my existing tests. Let's call it:

public void Point_remembers_its_x()
{
    var point = CreateAPoint();
    point.X.Should().Be(32);
} 
Enter fullscreen mode Exit fullscreen mode

But we have a mystery guest here. Why should X be 32?

  • That's not readable and causes Yoyo Problem.
  • That's not safe. What if I change the implementation of the CreateAPoint() factory method?
  • And as mentioned before, that's not trustworthy.

Trustworthiness can be addressed by test builders. But Value Objects don't usually need separate test specific builders. Value Objects can act as builders of themselves. It's best not to add redundant Value Object builders in unit test projects. Value Objects are linguistically allowed to act as builders of themselves, even if some of the builder methods never get called by production code.
Let's dig deeper in the next post and see more detailed examples of how to build clean Value Objects in unit tests.

Top comments (0)