In previous post we saw how changes to constructors can turn into a burden of shotgun surgery of many unit tests. Calling constructors in unit tests can also jeopardize the trustworthiness of unit tests in the long run. In this post we will see some refactoring proof examples of creating Value Objects for tests.
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 said test builders can be useful but almost always it's best not to use test specific builders for building 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_remembers_its_x()
{
var point = CreateAPoint().ButWithX(32);
point.X.Should().Be(32);
}
Now the factory method CreateAPoint()
creates a valid default Value Object outside of the scope of the test method's body, and customizing the created Point is done locally inside of the test method's body using the Value Object's API. Calling CreateAPoint().ButWithX(32)
makes the test yell at the reader that the only thing important thing in this scenario is X
, no matter what Y
or Z
are, no matter what any additional future constructor parameters will be, and no matter how the Value Object gets created. How an instance of a Point should be created is not the responsibility of the test. The test asks CreateAPoint()
to take care of that.
Which class does the factory method belong to?
What if we want to create the Value Object to be used by multiple test classes? We definitely don't like to do this:
PointTests.CreateAPoint()
The creation of a Value Object does not fit into the responsibilities of a test class. A value object may be used by multiple aggregates that each have a separate unit test class.
The factory method is better to be extracted into a new class which is responsible for creating multiple (or all) Value Objects needed by any of the unit tests.
The class may be alarming to be bloated at first glance, but breaking it down does not really add value.
public static class A
{
public static Point Point => new Point(32, -9);
}
You got the idea.
Let's call it:
public void Point_remembers_its_x()
{
var point = A.Point.ButWithX(32);
point.X.Should().Be(32);
}
Much better! The language is shining.
There is yet another way of avoiding calling constructors in unit tests. We can use data generators for many scenarios. In dotnet Autofixture library can be used to create a random Value Object:
public void point_remembers_its_x()
{
var point = new Fixture().Create<Point>().ButWithX(32);
point.X.Should().Be(32);
}
Rather than calling test data generators directly from the unit test method's body, hide the random creation of test data behind the interface of the A
class and let the A
class decide how to create the object:
public static class A
{
public static Point Point => new Fixture().Create<Point>();
}
Since a valid X
is between -90 and +90 and Y
should be from -180 to 180 the test data generator should be configured to generate the Value Object correctly which is outside of the scope of this post.
public void Point_remembers_its_x()
=> A.Point
.ButWithX(32)
.X.Should().Be(32);
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);
}
The A
class encapsulates the creation of more complex Value Objects. The factory methods can be easily composed inside the A
class.
Summery
- Direct object instantiation in unit test method bodies can end up fragile and untrustworthy safety nets.
- No safety net is a lot of the time better than an untrustworthy one.
- Value Objects can be used as test data builders.
- How to build Value Objects is rarely the responsibility of unit tests.
- Test data generators are best to be kept outside of unit test method bodies.
Full runnable project sample for this post can be found on this github repository.
Thanks for reading.
Top comments (0)