The TestDataBuilder pattern is used in unit testing to create collaborators. Its advantages versus using constructors:
- Changes to constructors are localized.
- Test code becomes clearer, because you don’t have to specify all the required parameters to the constructor.
A disadvantage is that the usage is somewhat lengthy which decreases readability:
var service = serviceBuilder.WithRepo(
repoBuilder.WithEntities(
new [] { entityBuilder.WithName("name") }).Build())
.WithMailSender(mailSenderBuilder.Build())
.Build();
- The word “With” is typically repeated many times.
- Same for the call to
Build()
. - Same for the word
Builder
.
Although all of them can be mitigated, I guess the solutions themselves have drawbacks. For instance, we could rename serviceBuilder
to service
, but this could give the wrong impression about the type of the variable. We could implement an implicit conversion from a builder class to the object it builds, but it is more code in the builder in addition to already abundant With*
methods.
Is there an alternative to TestDataBuilder that doesn’t have these drawbacks?
1
While I hope I can answer parts of this question in a useful fashion, I do feel the need to question parts of the premise.
- First, the repetition of
With[...]
andBuild()
doesn’t have to be a problem. After all, software development productivity is hardly measured by how quickly you can type code, and from a perspective af reading, repetition can sometimes aid in understanding, because it adds structure to code. - Second, while I realise that the above example is only that: an example, I don’t think it’s bad to have a couple of
Build
steps. If, on the other hand, you have a lot of such code, it may not indicate a problem with the Test Data Builder pattern itself, but rather it may be a smell that the test in question is too coarse-grained, or attempting to test too much.
Depending on your language, you can use various tricks. In C#, for example, you can avoid the need for the Build()
method by adding explicit or implicit conversions between the Builders and their target classes.
Still, I think a better SUT API can address the problems in a better way. For instance, considering the above example, you could design the SUT so that you could simply write the code like this:
var service = defaultService
.WithRepo(defaultRepo.WithEntities(
new [] { defaultEntity.WithName("name") })
.WithMailSender(defaultMailSender);
That is, the SUT API itself has With[...]
methods, while the default[...]
objects are test-specific default objects.
Do notice, however, that this particular example says WithMailSender(defaultMailSender)
, which I’d expect to be redundant, so that the example could probably be reduced even further:
var service = defaultService
.WithRepo(defaultRepo.WithEntities(
new [] { defaultEntity.WithName("name") });
I’ve outlined various options in an article series about the topic.
3
My suggestion is to use the Factory pattern with a twist, taking advantage of default parameters. The implementation is more concise than that of a builder, the test code too.
I call it MotherFactory
. Here is the code:
// ==== In a common test library ===
public abstract class MotherFactory
{
}
// ==== In a test project ===
public static class MotherFactoryEntityExtensions
{
public static Entity Entity(
this MotherFactory a,
int? year = 2014,
string name = "name",
Dependency dependency = null,
withoutDependency = false)
{
if (withoutDependency && dependency != null)
{
throw new ArgumentException(
"The parameter 'dependency' cannot be used when true is specified for 'withoutDependency'.",
"dependency");
}
if(dependency == null && !withoutDependency)
{
dependency = a.Dependency();
}
return new Entity(startYear, endYear, name, summary, dependency);
}
}
public class EntityTest
{
private static readonly MotherFactory an = null;
[Fact]
public void Ctor_GivenNullDependency_Throws()
{
Exception e = Record.Exception(() =>
{
Entity systemUnderTest = an.Entity(withoutDependency: true);
});
Assert.NotNull(e);
}
}
public class ServiceTest
{
private static readonly MotherFactory a = null;
private static readonly MotherFactory an = null;
[Fact]
public void Find_GivenCorrectRequest_FindsEntity()
{
var service = a.Service(repo: a.Repository(dbEntities: new [] { an.Entity(name: "XXX") });
var result = service.Find();
Assert.Equal("XXX", result.Name);
}
}
Notes
- Put the
MotherFactory
class in a library and in test projects create a class with extension methods for each collaborator class. - Parameters that have a prefix
without
(likewithoutDependency
) are used only in cases when we want to disable creating the default instance of a dependency and passnull
instead. - A
MotherFactory*Extensions
class can also be an ObjectMother by providing methods likeUserWithCompleteProfile()
.
2