loading...

Your value objects are your test data builders

vlerx profile image Mohsen Bazmi Updated on ・3 min read

In previous post we saw how constructors can turn into a burden of shotgun surgery for our unit tests. In this post we will see specific examples of how to create value objects that our tests need without getting into those problems.

In previous post we saw the Point value object with the following constructor :

var point = new Point(x : 32, y : -9);

Now we know why calling it in the unit test bodies is a bad idea. We also went through a couple of workarounds to avoid calling them which may suffice in some situations. One of the solutions was calling a factory:

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

In this post we will see more ways of creating test value objects, and finally add some language around the pattern in order to give the constructors more freedom to change later on.

As I saied test builders can be useful but I don't like to use builder for my value objects. Your value objects are your test builders:

public class Point
{
    public Point(double x, double y, double z = 0)
    {
       ...
    }
    public Point ButWithX(double alternativeX)
    {
        return new Point(alternativeX, this.Y, this.Z);
    }
...

Let's call it:

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

Now the factory method CreateAPoint() is creating a default value object for us outside of the scope of the test body, and test specific customizations are done locally inside of the test's body. by calling CreateAPoint().ButWithX(32) the test is yelling at the reader that the only thing that's important to test this scenario is X, I don't care about Y and Z, or the way the point is being created, and since that's out side of my responsibility I ask CreateAPoint() to do it for me. But wait! Which class does the CreateAPoint() factory method belong to? What if we want to create the value object to be used by multiple tests classes? We definitely don't do this:

PointTests.CreateAPoint()

The creation of a value object is not fit into the responsibilities of a test class. A value object my be used by multiple aggregates each with their own unit test class.

I would extract it into another class which is responsible for creating multiple (or maybe all) value objects that's needed by any unit test in the project. That class seems bloated when you hear, but as far as I'm concerned breaking it down does not really add value. It also lets us have some sort of language around the pattern.

public static class A
{
    public static Point Point => new Point(32, -9);
}

You got the idea.
Let's call it:

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

Much better! Do you see how the language is shining?
There is yet another way of avoiding calling constructors in unit tests. We can use data generators for many scenarios. Let's ask the Autofixture to create the test value object as an example:

public void Point_Accepts_X_When_It_Is_In_The_Right_Range()
{
    var point = new Fixture().Create<Point>().ButWithX(32);
    point.X.Should().Be(32);
}

I don't like to call the test data generators from my unit test body. I prefer to call the A class and let that class decide how to create the object:

public static class A
{
    public static Point Point => new Fixture().Create<Point>();
}

And don't forget that X should be between -90 and +90 and Y should be from -180 to 180, so the Fixture needs to be configured to generate the value object correctly which is outside of the scope of this post.

We can also add more methods to the A class:

public static class A
{
    public static Point Point => new Fixture().Create<Point>();
    public static Relocation Relocation => new Relocation(from: Point, to:Point);
}

As we see the class A can act as a composition root that composes more complex value objects easily.

Summery

  • Blindly object instantiation in unit test body is disastrous.
  • Value object builders are different from other types of builders.
  • Don't over engineer. Your value objects are your test builders.
  • Building objects is rarely the responsibility of unit tests.
  • Keep test data generators out side of unit test body.

One more thing: some languages support immutable objects. C# 9's init-only properties and record types can be used as value objects, so you don't need to write the factories yourself anymore.
Full runnable project sample can be found on this github repository.

Thanks for reading.

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