loading...

Don’t call constructors in unit tests

vlerx profile image Mohsen Bazmi Updated on ・3 min read

Writing good tests is tricky. Implementation details should be changeable independently without breaking the tests. Constructors are usually Implementation details. They also change more frequently than the methods. I'm going to quickly show examples of how calling constructors in unit tests can turn into a disaster, then provide different 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);

And add a simple unit test for it:

public void Point_Accepts_X_When_It_Is_In_The_Right_Range()
{
    var point = new Point(x : 32, y : -9);
    point.X.Should().Be(32);
}

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. So I 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_Does_Not_Accept_X_If_It_Is_Greater_Than_90()
{
    Action newPoint = () => new Point(x: 92, y: -9);
    newPoint.Should().Throw<ArgumentOutOfRangeException>();
}

There are also other tests:

  • Point does not accept the X if it is smaller than -90.
  • Point does not accept the Y if it is greater than -180.
  • Point does not accept the Y if it is smaller than -180.
  • Point accepts Y when it is in the right range. I omit writing the tests in the post as they are straightforward. So we have 6 tests so far and each call the constructor of the Point. Then a new requirement emerges stating that Z should also be a part of Point. Oh no! 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, and leave the test with passing Y as X parameter to the constructor somewhere, and that change ends up with unit tests that should not pass but they are passing which dramatically increases the chance for introduction of bugs.

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

public class Point
{
    public Point(double x, double y, double z = 0)
    {
...

That works here. We no more need to add the parameters to all of our tests, but there are still other problems that we may face when unit testing less obvious domains. For example another parameter may emerge that needs to be placed between the existing parameters, and as we know many programming languages (like C#) do not support optional parameters before required ones.

Another solution would be extracting the general creation of that the object to a factory method, and keep the customizations of the object local.

public Point CreateAPoint()
{
    return new Point(32, -9);
}

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. Seems interesting. Let's call it:

public void Point_Accepts_X_When_It_Is_In_The_Right_Range()
{
    var point = CreateAPoint();
    point.X.Should().Be(32);
} 

But wait! 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.

Well, that can be addressed by test builders. but I don't use them for immutable value objects. The value objects can act as builders of themselves. I don't duplicate an extra test builder in my unit test project. Even if I don't call them from production code, the value objects linguistically allowed to act as builder of themselves.
Let's dig deeper in the next post and see more detailed examples of how to create value objects for unit tests as cleanly as possible.

Posted on by:

vlerx profile

Mohsen Bazmi

@vlerx

10 years experienced developer, passionate about software architecture, with background of using multiple languages, methodologies, and technologies.

Discussion

pic
Editor guide