diff --git a/README.md b/README.md index a783c4d..9836c7d 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,13 @@ To use this package, simply install the NuGet **AbstractBuilder**. It has no ext Alternatively, you can visit the NuGet webpage for **AbstractBuilder**: https://www.nuget.org/packages/AbstractBuilder/. -## How does it works +## Getting started -We start with a default builder and modify it as needed. Each modification creates a **new builder** that inherits the previous changes. When we call the `Build` method, we create the object. +When you have to create a test, you ofen reuse objects with the same information. Instead of copying and pasting, you can create a method to build those test objects multiple times. + +To simplify this process and follow the principle of single responsibility, builders are used. The classical approach is that one builder contains only one object. However, **AbstractBuilder** modifies the pattern to create a new builder every time a modification is added, and the final object is not created until the `Build` method is called. + +Consequently, a builder from AbstractBuilder can be declared in your test class only once and reused in all the tests you need. ```csharp @@ -31,6 +35,31 @@ In the previous example, we make two changes and create two `Person` objects. To use the `AbstractBuilder` class, you need to inherit from it and specify the generic type as the result. The build steps can vary in complexity, as shown in this example. +```csharp + + public MyBuilder : AbstractBuilder + { + public MyBuilder WithName(string name) + { + return Set(x => x.Name = name); + } + + protected override Person CreateDefault() + { + return new Person { + IsAlive = true + }; + } + } + +``` + +## How can I use it? - Way 2: Heritage (before version 1.7.0) + +In previous versions of AbstractBuilder, the mandatory constructor should be provided, regardless of its visibility. The current version is backward compatible, and you can still use it. However, it is recommended to move to _Way 1_ to simplify your builders. + +Your builder requires a public constructor (the default constructor in the example) and another constructor with the seed (the visibility does not matter). In this example, we can set the "*Name*" property with the `WithName` method. We should always use the `Set<>` method to create new methods that modify the builder. + ```csharp public MyBuilder : AbstractBuilder @@ -49,7 +78,7 @@ To use the `AbstractBuilder` class, you need to inherit from it and specify the { return Set(x => x.Name = name); } - + private static Result CreateDefaultValue() { return new Person { @@ -57,12 +86,9 @@ To use the `AbstractBuilder` class, you need to inherit from it and specify the }; } } - ``` -Your builder requires a public constructor (the default constructor in the example) and another constructor with the seed (the visibility does not matter). In this example, we can set the "*Name*" property with the `WithName` method. We should always use the `Set<>` method to create new methods that modify the builder. - -## How can I use it? - Way 2: Using the abstract builder itself +## How can I use it? - Way 3: Using the abstract builder itself You can use it directly without any restrictions. However, you will need to declare everything and you cannot reuse the builder. @@ -77,7 +103,7 @@ You can use it directly without any restrictions. However, you will need to decl ``` -## Could we aggregate some modifications in one call? +## Can we aggregate some modifications in one call? We can do either of these: call the lambda action multiple times or use brackets. @@ -163,7 +189,92 @@ This builder supports both record class and record struct. ``` -In the previous example, alpha was _(10,20,0)_, beta was _(10,20,10)_ and charlie was _(10,20,20)_. +In the previous example, `alpha` was _(10,20,0)_, `beta` was _(10,20,10)_ and `charlie` was _(10,20,20)_. + +## Simplifying tests + +### Create a static field + +You can create the builder as a static field in your test class and reuse it in your tests. + +```csharp + using AbstractBuilder; + using Xunit; + + public class BookCiteDomainServiceTests + { + private static BookCiteBuilder BookCiteBuilder = new(); + + public MyTests() + { + _service = new BookCiteDomainService(); + } + + [Theory] + [InlineData("Arthur", "Conan Doyle")] + [InlineData("Camilo", "José Cela")] + public void PerformAction_SomeConditions_ExpectedResults(string name, string surname) + { + // Arrange + BookCite bookCite = BookCiteBuilder + .WithAuthor("Arthur", ); + + // Act + var actual = _service.PerformAction(bookCite); + + // Assert + // Checking of the expected values + } + + [Fact] + public void PerformAction_SomeOtherConditions_OtherExpectedResults() + { + // Arrange + BookCite bookCite = BookCiteBuilder + .WithAuthor("Arthur", null); + + // Act + var actual = _service.PerformAction(bookCite); + + // Assert + // Checking of the expected values + } + } +``` + +### Injecting builders with IClassFixture in xUnit + +If you are afraid of adding `new` everywhere in the code, you can use `IClassFixture` to inject it. + +```csharp + using AbstractBuilder; + using Xunit; + + public class BookCiteDomainServiceTests : IClassFixture + { + private BookCiteBuilder _bookCiteBuilder; + + public MyTests(BookCiteBuilder bookCiteBuilder) + { + bookCiteBuilder = _bookCiteBuilder; + _service = new BookCiteDomainService(); + } + + [Fact] + public void PerformAction_SomeOtherConditions_OtherExpectedResults() + { + // Arrange + BookCite bookCite = BookCiteBuilder + .WithAuthor("Arthur", null); + + // Act + var actual = _service.PerformAction(bookCite); + + // Assert + // Checking of the expected values + } + } +``` --- diff --git a/src/AbstractBuilder/AbstractBuilder.cs b/src/AbstractBuilder/AbstractBuilder.cs index 03e6f95..eb2a946 100644 --- a/src/AbstractBuilder/AbstractBuilder.cs +++ b/src/AbstractBuilder/AbstractBuilder.cs @@ -20,15 +20,19 @@ public class AbstractBuilder /// /// Initializes a new instance of the class. /// + /// Default constructor. /// Method to create a new instance - public AbstractBuilder(Func seedFunc) + public AbstractBuilder(Func seedFunc = null) { if (seedFunc == null) { - throw new ArgumentNullException(nameof(seedFunc)); + _seedFunc = _ => CreateDefault(); + } + else + { + _seedFunc = _ => seedFunc(); } - _seedFunc = _ => seedFunc(); _modifications = new Queue>(); } @@ -149,7 +153,7 @@ public virtual async Task BuildAsync(BuilderContext builderContext = nu { var currBuilderContext = builderContext ?? new BuilderContext(); var cancelTkn = currBuilderContext.CancellationToken; - + cancelTkn.ThrowIfCancellationRequested(); TResult obj = await Task.Run(() => _seedFunc(currBuilderContext), cancelTkn); @@ -162,23 +166,91 @@ public virtual async Task BuildAsync(BuilderContext builderContext = nu return obj; } + /// + /// Creates an instance of using various constructor strategies. + /// + /// An instance of . + /// Thrown when no suitable constructor is found. private AbstractBuilder CreateBuilder() { - var type = GetType(); + if (TryCreateBuilderWithBuilderContext(out AbstractBuilder result)) + { + return result; + } - ConstructorInfo ctor = type.GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func) }, null); + if (TryCreateBuilderWithFunction(out result)) + { + return result; + } + + if (TryCreateBuilderWithDefaultCtor(out result)) + { + return result; + } + + throw new MissingMethodException(GetType().Name, CtorConstants.MethodName); + } + + /// + /// Attempts to create an instance of using a constructor that accepts a . + /// + /// The type of the builder. + /// The created builder instance, if successful. + /// true if the builder was successfully created; otherwise, false. + private bool TryCreateBuilderWithBuilderContext(out AbstractBuilder result) + { + ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func) }, null); if (ctor != null) { - return (AbstractBuilder)ctor.Invoke(new object[] { _seedFunc }); + result = (AbstractBuilder)ctor.Invoke(new object[] { _seedFunc }); + return true; } - ctor = type.GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func) }, null) - ?? throw new MissingMethodException(GetType().Name, CtorConstants.MethodName); + result = null; + return false; + } - TResult SeedFuncWithoutCancellation() => _seedFunc(null); + /// + /// Attempts to create an instance of using a constructor that accepts a . + /// + /// The type of the builder. + /// The created builder instance, if successful. + /// true if the builder was successfully created; otherwise, false. + private bool TryCreateBuilderWithFunction(out AbstractBuilder result) + { + ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func) }, null); + + if (ctor != null) + { + TResult SeedFuncWithoutCancellation() => _seedFunc(null); + + result = (AbstractBuilder)ctor.Invoke(new object[] { (Func)SeedFuncWithoutCancellation }); + return true; + } + + result = null; + return false; + } + + /// + /// Attempts to create an instance of using a parameterless constructor. + /// + /// The type of the builder. + /// The created builder instance, if successful. + /// true if the builder was successfully created; otherwise, false. + private bool TryCreateBuilderWithDefaultCtor(out AbstractBuilder result) + { + ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, Type.EmptyTypes, null); + + if (ctor != null) + { + result = (AbstractBuilder)ctor.Invoke(null); + return true; + } - return (AbstractBuilder)ctor.Invoke(new object[] { (Func)SeedFuncWithoutCancellation }); + result = null; + return false; } /// @@ -192,5 +264,14 @@ private bool IsSupported() Type currentType = GetType(); return resultType == currentType || resultType.IsInstanceOfType(currentType); } + + /// + /// Creates a default instance of . + /// + /// It is not used when a seed function is provided. + protected virtual TResult CreateDefault() + { + return default; + } } } diff --git a/src/AbstractBuilder/AbstractBuilder.csproj b/src/AbstractBuilder/AbstractBuilder.csproj index 034554a..09079a9 100644 --- a/src/AbstractBuilder/AbstractBuilder.csproj +++ b/src/AbstractBuilder/AbstractBuilder.csproj @@ -12,9 +12,9 @@ https://github.com/p-caballero/AbstractBuilder GIT Copyright (c) 2020-2024 Pablo Caballero - 1.6.0.0 - 1.6.0.0 - 1.6.0 + 1.7.0.0 + 1.7.0.0 + 1.7.0 README.md diff --git a/tests/UnitTests/AbstractBuilderTests.cs b/tests/UnitTests/AbstractBuilderTests.cs index f9ab05c..1a4dfe9 100644 --- a/tests/UnitTests/AbstractBuilderTests.cs +++ b/tests/UnitTests/AbstractBuilderTests.cs @@ -171,16 +171,23 @@ public void Set_EmptyModifications_ReturnsSimilarBuilder() } [Fact] - public void Set_BuilderWithoutSeedCtor_ThrowsMissingMethodException() + public void Set_BuilderWithoutSeedCtor_UsesDefaultConstructorWithLambdaCreation() { // Arrange var builder = new BuilderWithoutSeedCtor(); // Act & Assert - Assert.Throws(() => + var actual = builder.Set(x => x.NumDoors = 5) + .Build(); + + // Assert + Assert.Equivalent(new { - builder.Set(x => x.NumDoors = 5); - }); + Id = 0, + Model = Car.DefaultModel, + NumDoors = 5, + Color = Car.DefaultColor + }, actual); } [Fact] diff --git a/tests/UnitTests/DieselBmwBuilderTests.cs b/tests/UnitTests/DieselBmwBuilderTests.cs new file mode 100644 index 0000000..1ff6c5b --- /dev/null +++ b/tests/UnitTests/DieselBmwBuilderTests.cs @@ -0,0 +1,46 @@ +namespace AbstractBuilder +{ + using System.Drawing; + using AbstractBuilder.Examples.Builders; + using Xunit; + + public class DieselBmwBuilderTests : IClassFixture + { + private readonly DieselBmwBuilder _dieselBmwBuilder; + + public DieselBmwBuilderTests(DieselBmwBuilder dieselBmwBuilder) + { + _dieselBmwBuilder = dieselBmwBuilder; + } + + [Fact] + public void Build_EmptyCtor_BuildsDefaultObject() + { + // Act + var actual = _dieselBmwBuilder.Build(); + + // Assert + Assert.Equivalent(new + { + Color = Color.SlateGray.Name, + Model = "318d", + NumDoors = 3 + }, actual); + } + + [Fact] + public void Build_EmptyCtorAndOneSet_BuildsDefaultObject() + { + // Act + var actual = _dieselBmwBuilder.WithNumDoors(5).Build(); + + // Assert + Assert.Equivalent(new + { + Color = Color.SlateGray.Name, + Model = "318d", + NumDoors = 5 + }, actual); + } + } +} diff --git a/tests/UnitTests/Examples/Builders/DieselBmwBuilder.cs b/tests/UnitTests/Examples/Builders/DieselBmwBuilder.cs new file mode 100644 index 0000000..1dda016 --- /dev/null +++ b/tests/UnitTests/Examples/Builders/DieselBmwBuilder.cs @@ -0,0 +1,26 @@ +namespace AbstractBuilder.Examples.Builders +{ + using System.Drawing; + using AbstractBuilder.Examples.Entities; + + public sealed class DieselBmwBuilder : AbstractBuilder + { + private static int _lastId; + + public DieselBmwBuilder WithNumDoors(int numDoors) + { + return Set(x => x.NumDoors = numDoors); + } + + protected override Car CreateDefault() + { + return new Car + { + Id = ++_lastId, + Color = Color.SlateGray.Name, + Model = "318d", + NumDoors = 3 + }; + } + } +} diff --git a/tests/UnitTests/Examples/Entities/Car.cs b/tests/UnitTests/Examples/Entities/Car.cs index b501b46..e6e1e8f 100644 --- a/tests/UnitTests/Examples/Entities/Car.cs +++ b/tests/UnitTests/Examples/Entities/Car.cs @@ -1,8 +1,6 @@ namespace AbstractBuilder.Examples.Entities { - using System; - - internal class Car + public class Car { public const string DefaultModel = "UNKNOW MODEL";