I'm excited to announce the release of EasyTdd 0.4.0! The major highlight of this release is the introduction of the Incremental Builder, implemented using the IIncrementalGenerator
technology.
The Builder Pattern is a creational design pattern that provides a way to construct complex objects step by step. It allows for the creation of different representations of an object using the same construction process. This pattern is particularly useful in test-driven development (TDD) because it simplifies the creation of test data and makes tests more readable and maintainable by providing a clear and fluent interface for constructing test objects.
One of the biggest benefits of using a builder in TDD is the ability to predefine specific valid setups, such as "Default" or "Typical," along with other specific cases. In a test, you can use these predefined setups or modify just one or two fields as needed. This approach ensures that your tests are consistent and easy to understand, focusing on the behavior being tested rather than the details of object creation.
IIncrementalGenerator is a powerful feature in C# supported in .NET 6.0 and later. It enhances source generation by processing and updating only the parts of the code that have changed, with the code being generated in the background.
How to use it?
Let's say we have a sample class:
namespace EasyTdd.CodeSource.CodeProject1.ForIncrementalBuilder
{
public class SampleWithGeneric<T>
where T: class, IEnumerable
{
public SampleWithGeneric(T someProperty, string someString)
{
SomeProperty = someProperty;
SomeString = someString;
}
public T SomeProperty { get; }
public string SomeString { get; set; }
public int SomeInt { get; set; }
public DateTime SomeDateTime { get; set; }
}
}
In Visual Studio with EasyTdd installed, place the cursor on the class for which you want to create a builder. Open the Quick Action menu (either by clicking the light bulb with the mouse or pressing Ctrl+. on the keyboard) and select "Generate Incremental Builder."
This action will produce a partial builder class with the BuilderFor attribute set:
namespace EasyTdd.CodeSource.CodeProject1.TestDoubles.Builders.ForIncrementalBuilder
{
[EasyTdd.Generators.BuilderFor(typeof(SampleWithGeneric<>))]
public partial class SampleWithGenericBuilder<T>
{
public static SampleWithGenericBuilder<T> Default()
{
return new SampleWithGenericBuilder<T>(
() => default, // Set default someProperty value
() => default, // Set default someString value
() => default, // Set default someInt value
() => default // Set default someDateTime value
);
}
}
}
This file is where the default and other known setups of the class can be defined. The templates only define the "default" setup, so feel free to change the default values and define as many setups as you need. This file is protected from regeneration and will not be affected by changes in the target class.
namespace EasyTdd.CodeSource.CodeProject1.TestDoubles.Builders.ForIncrementalBuilder
{
[EasyTdd.Generators.BuilderFor(typeof(SampleWithGeneric<>))]
public partial class SampleWithGenericBuilder<T>
{
public static SampleWithGenericBuilder<T> Default()
{
return new SampleWithGenericBuilder<T>(
() => default,
() => "",
() => 0,
() => DateTime.Now
);
}
public static SampleWithGenericBuilder<T> SomeSpecificCase(T value)
{
return new SampleWithGenericBuilder<T>(
() => value,
() => "some specific string",
() => 782,
() => DateTime.Parse("2024-05-20")
);
}
}
}
The rest of the class is generated by Roslyn, the .NET compiler platform. You can see the outcome by pressing F12 (or checking Edit > Go To Definition if your shortcut scheme has changed) on the SampleWithGenericBuilder<T>:
The code in SampleWithGenericBuilder.g.cs looks as following:
namespace EasyTdd.CodeSource.CodeProject1.TestDoubles.Builders.ForIncrementalBuilder
{
public partial class SampleWithGenericBuilder<T>
where T : class, System.Collections.IEnumerable
{
private Func<T> _someProperty;
private Func<string> _someString;
private Func<int> _someInt;
private Func<System.DateTime> _someDateTime;
public static implicit operator EasyTdd.CodeSource.CodeProject1.ForIncrementalBuilder.SampleWithGeneric<T>(SampleWithGenericBuilder<T> builder) => builder.Build();
public SampleWithGenericBuilder(
Func<T> someProperty,
Func<string> someString,
Func<int> someInt,
Func<System.DateTime> someDateTime)
{
_someProperty = someProperty;
_someString = someString;
_someInt = someInt;
_someDateTime = someDateTime;
}
public SampleWithGenericBuilder(
T someProperty,
string someString,
int someInt,
System.DateTime someDateTime)
{
_someProperty = () => someProperty;
_someString = () => someString;
_someInt = () => someInt;
_someDateTime = () => someDateTime;
}
public EasyTdd.CodeSource.CodeProject1.ForIncrementalBuilder.SampleWithGeneric<T> Build()
{
return new EasyTdd.CodeSource.CodeProject1.ForIncrementalBuilder.SampleWithGeneric<T>(
_someProperty(),
_someString())
{
SomeInt = _someInt(),
SomeDateTime = _someDateTime()
};
}
public SampleWithGenericBuilder<T> WithSomeProperty(Func<T> value)
{
_someProperty = value;
return this;
}
public SampleWithGenericBuilder<T> WithSomeProperty(T value)
{
return WithSomeProperty(() => value);
}
public SampleWithGenericBuilder<T> WithSomeString(Func<string> value)
{
_someString = value;
return this;
}
public SampleWithGenericBuilder<T> WithSomeString(string value)
{
return WithSomeString(() => value);
}
public SampleWithGenericBuilder<T> WithSomeInt(Func<int> value)
{
_someInt = value;
return this;
}
public SampleWithGenericBuilder<T> WithSomeInt(int value)
{
return WithSomeInt(() => value);
}
public SampleWithGenericBuilder<T> WithSomeDateTime(Func<System.DateTime> value)
{
_someDateTime = value;
return this;
}
public SampleWithGenericBuilder<T> WithSomeDateTime(System.DateTime value)
{
return WithSomeDateTime(() => value);
}
}
}
I want to note that generic constraints are added automatically. The generator takes into account which properties are set using the constructor and which have only setter properties.
Usage of the builder
The beauty and power of the builder pattern lie in its flexibility and simplicity. When using a builder, I do not need to specify all the properties—only the ones relevant to a test—even when the constructor of the type is complex and requires many fields to be set. This makes the builder especially useful in scenarios where the type has a heavy constructor with numerous parameters. By using a builder, I can create objects with only the necessary properties, making my tests more focused and easier to read. This approach not only reduces boilerplate code but also enhances the maintainability and clarity of the test suite.
public void Demo()
{
var orderSample = SampleWithGenericBuilder<List<Order>>
.SomeSpecificCase(new List<Order>())
.WithSomeString("Order list case");
var defaultSample = SampleWithGenericBuilder<List<Item>>
.Default();
var listOfSamples = new List<SampleWithGenericBuilder<List<Item>>>
{
SampleWithGenericBuilder<List<Item>>
.Default()
.WithSomeInt(1),
SampleWithGenericBuilder<List<Item>>
.Default()
.WithSomeInt(2),
SampleWithGenericBuilder<List<Item>>
.Default()
.WithSomeInt(3),
SampleWithGenericBuilder<List<Item>>
.Default()
.WithSomeInt(4),
};
}
How it works?
The solution consists of three parts: EasyTdd, the Visual Studio extension itself; the settings and template files; and the EasyTdd.Generators NuGet package. When you click "Generate Incremental Builder," EasyTdd considers the settings to determine where the builder needs to be placed, creates the file, and renders the content using a template specified in the settings. If the EasyTdd.Generators NuGet package is not already installed in the target project, EasyTdd installs it. EasyTdd.Generators contains the logic to create the BuilderFor attribute class and generate the builder class for a type defined in the BuilderFor parameter. The rest is accomplished by Roslyn, the .NET compiler platform.
Configuration
The default placement, naming, and location of the generated builder may not suit everyone's preferences and requirements. Fortunately, all of these aspects can be easily customized to better align with your needs and tastes. Now, I will provide the settings one by one. Settings for the incremental builder, as well as for all other tools, are located in the settings.json
file under the IncrementalTestDoubleForConcrete
section. Here are the descriptions:
ClassSuffix: The default value is
Builder
. This value determines the suffix added to the target class's corresponding builder class name. In the previous example, forSampleWithGeneric
, the builder class name wasSampleWithGenericBuilder
.NameInMenu: The default value is
Generate Incremental Builder
. This setting allows you to modify the name displayed in the Quick Actions menu. By default, it isGenerate Incremental Builder
, but you can rename it to whatever you prefer.AssemblySpecificTargetProjectSuffix: The default value is
TestDoubles
. This setting instructs EasyTdd to place the generated builder in a project corresponding to the project of the target class. It searches for a project with the same name plus the predefined suffix. For example, ifSampleWithGeneric
is in the projectEasyTdd.CodeSource.CodeProject1
, the corresponding project for the builder would beEasyTdd.CodeSource.CodeProject1.TestDoubles
. This can be customized to suit individual preferences and requirements, such as using a different suffix for the test doubles project. Within the test doubles project, EasyTdd maintains the original folder hierarchy.TargetProjectNameForAssembliesWithoutSpecificTargetProject: The default value is
null
. This setting specifies a project name where a test double will be placed when the project of a target class doesn't have a corresponding test double project. For example, if you want to place all test doubles from all projects into a single test doubles project calledEasyTdd.CodeSource.TestDoubles
, you would set this setting toEasyTdd.CodeSource.TestDoubles
. In this case, you can leaveAssemblySpecificTargetProjectSuffix
with its default value, as EasyTdd will first try to find a project with the predefined suffix and then fall back to the value inTargetProjectNameForAssembliesWithoutSpecificTargetProject
if it doesn't find one. If EasyTdd doesn't locate the project specified inTargetProjectNameForAssembliesWithoutSpecificTargetProject
, it will place the test double class next to the target class.Folder: The default value is
Builders
. This setting allows you to organize test doubles, such as builders and mocks, into corresponding folders. For example, if you choose to have a single project for all test doubles and setTargetProjectNameForAssembliesWithoutSpecificTargetProject
toEasyTdd.CodeSource.TestDoubles
andFolder
to "Builders," then the builder forEasyTdd.CodeSource.CodeProject1\Models\SampleWithGeneric.cs
will be placed inEasyTdd.CodeSource.TestDoubles\Builders\CodeProject1\Models\SampleWithGeneric.cs
.-
FileTemplates: This section contains settings for the specific files that will be generated.
-
NameTemplate: The default value is
{{className}}.cs
for the EasyTdd-generated part, and{{className}}.g.cs
for the EasyTdd.Generators-generated part. Here,className
refers to the name of the builder class. -
ContentTemplateFile: The default value for the EasyTdd-generated part is
DefaultTemplates\incremental.builder.tpl
. The default value for the EasyTdd.Generators part isDefaultTemplates\incremental.builder.g.tpl
. -
Recreate: Files marked with
true
are generated by the incremental generator, EasyTdd.Generators, while files marked withfalse
are generated by the EasyTdd Visual Studio extension. The files generated by EasyTdd are intended for modifications, such as builder setups. In contrast, the files generated by EasyTdd.Generators will be regenerated whenever changes are made to the target class.
-
NameTemplate: The default value is
ToolingNamespaces: By default, this contains a list of namespaces to support the tooling used in the default test template. EasyTdd adds target class-related namespaces to the generated class, but it is not aware of the tooling used in the template itself. This is where you can define those namespaces. By default, the
System
namespace is added to supportFunc
in the template.
Feel free to update the templates to match your taste and needs. Remember to copy and paste them outside of the DefaultTemplates
folder, as the templates in this folder are overridden with each new release.
Summary
In this blog post, I introduced the new version of EasyTdd. This new version includes a powerful tool—the incremental builder—which is automatically updated whenever the target class changes. The code generation is highly configurable and easily changeable, allowing you to tailor it to your specific needs. Additionally, I discussed how the builder pattern and IIncrementalGenerator enhance the test-driven development experience by streamlining the creation of test data and improving build performance. This tool takes types with generic parameters into account and works seamlessly with classes where values are set using constructors, setter properties, or a mix of both. With these new features, EasyTdd offers a more efficient and scalable approach to managing test doubles and supporting large projects.
Top comments (4)
Could you tell the advantages of this builder over Bogus test data generator?
Hello,
I'm not an extensive Bogus user, and I might be mistaken, but I see these two as serving different purposes. According to the Bogus documentation, it is a fake data generator. On the other hand, a builder in TDD is used to provide objects with specific values for properties that are relevant to the test. For example, if I'm writing a test for a piece of functionality that should behave a certain way when an invoice has a VAT percentage of 21%, I would create it like this:
var invoice = InvoiceBuilder.Default().WithVAT(21)
. Of course, other specific invoices could be used instead of the default.I believe something similar can be achieved with Bogus. However, EasyTdd offers additional tooling if you are a Visual Studio user. With a few clicks, I can generate a class where I can set defaults, create other setups, and use the class throughout the solution. In fact, EasyTdd can work in cooperation with Bogus. For example, specific setups, such as Random or Fake, could be created where default values are set using Bogus.Faker. The comment section doesn't allow to include a code block, so simple example could look something like:
Thanks, that was my point. While Bogus excels in randomized data generation, nothing stops you from using the same tool to setup a specific value:
I appreciate your perspective. I've had plans to explore using Bogus for value creation within a Default setup, and I certainly acknowledge its strengths. However, I still feel compelled to advocate for EasyTdd, particularly after reflecting on your comment and experimenting further with Bogus. Here's what I discovered regarding the advantages of EasyTdd:
In numerous daily scenarios, I find it beneficial to have a builder as a separate class with predefined default values. This setup allows me to avoid manually setting these values for each test. Additionally, I can create specific setups for different cases—using an Order example, I can establish setups like Computer, Printer, and Headset for the OrderItemBuilder, which are tailored for specific test cases. While it's possible to achieve similar functionality with Bogus by creating a class that inherits from Faker, this method requires manual coding. In contrast, EasyTdd generates the bigger part of necessary code automatically.
Often, I require quick object creation with specific values for a few properties. With Bogus, I can easily override the default rule by using RuleFor on the desired property. However, I have not found a way to override the initial setup when an object is instantiated by a constructor. EasyTdd provides a consistent approach, whether dealing with constructor parameters or setter properties.
In many instances, I prefer not to use random values in test-driven development. When randomness is necessary, I opt for the method I described using Bogus in my initial comment.