Skip to content

Commit

Permalink
feat: AbstractBuilder can use the parameterless constructor.
Browse files Browse the repository at this point in the history
  • Loading branch information
p-caballero committed Sep 16, 2024
1 parent 1026599 commit ef62f3c
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 30 deletions.
129 changes: 120 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Person>
{
public MyBuilder WithName(string name)
{
return Set<MyBuilder>(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<Person>
Expand All @@ -49,20 +78,17 @@ To use the `AbstractBuilder` class, you need to inherit from it and specify the
{
return Set<MyBuilder>(x => x.Name = name);
}

private static Result CreateDefaultValue()
{
return new Person {
IsAlive = true
};
}
}

```

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.

Expand All @@ -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.

Expand Down Expand Up @@ -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<BookCiteBuilder>
{
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
}
}
```

---

Expand Down
103 changes: 92 additions & 11 deletions src/AbstractBuilder/AbstractBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ public class AbstractBuilder<TResult>
/// <summary>
/// Initializes a new instance of the <see cref="AbstractBuilder{TResult}"/> class.
/// </summary>
/// <remarks>Default constructor.</remarks>
/// <param name="seedFunc">Method to create a new instance</param>
public AbstractBuilder(Func<TResult> seedFunc)
public AbstractBuilder(Func<TResult> seedFunc = null)
{
if (seedFunc == null)
{
throw new ArgumentNullException(nameof(seedFunc));
_seedFunc = _ => CreateDefault();
}
else
{
_seedFunc = _ => seedFunc();
}

_seedFunc = _ => seedFunc();
_modifications = new Queue<Action<TResult, BuilderContext>>();
}

Expand Down Expand Up @@ -149,7 +153,7 @@ public virtual async Task<TResult> BuildAsync(BuilderContext builderContext = nu
{
var currBuilderContext = builderContext ?? new BuilderContext();
var cancelTkn = currBuilderContext.CancellationToken;

cancelTkn.ThrowIfCancellationRequested();
TResult obj = await Task.Run(() => _seedFunc(currBuilderContext), cancelTkn);

Expand All @@ -162,23 +166,91 @@ public virtual async Task<TResult> BuildAsync(BuilderContext builderContext = nu
return obj;
}

/// <summary>
/// Creates an instance of <see cref="AbstractBuilder{TResult}"/> using various constructor strategies.
/// </summary>
/// <returns>An instance of <see cref="AbstractBuilder{TResult}"/>.</returns>
/// <exception cref="MissingMethodException">Thrown when no suitable constructor is found.</exception>
private AbstractBuilder<TResult> CreateBuilder()
{
var type = GetType();
if (TryCreateBuilderWithBuilderContext(out AbstractBuilder<TResult> result))
{
return result;
}

ConstructorInfo ctor = type.GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func<BuilderContext, TResult>) }, null);
if (TryCreateBuilderWithFunction(out result))
{
return result;
}

if (TryCreateBuilderWithDefaultCtor(out result))
{
return result;
}

throw new MissingMethodException(GetType().Name, CtorConstants.MethodName);
}

/// <summary>
/// Attempts to create an instance of <see cref="AbstractBuilder{TResult}"/> using a constructor that accepts a <see cref="Func{BuilderContext, TResult}"/>.
/// </summary>
/// <param name="type">The type of the builder.</param>
/// <param name="result">The created builder instance, if successful.</param>
/// <returns><c>true</c> if the builder was successfully created; otherwise, <c>false</c>.</returns>
private bool TryCreateBuilderWithBuilderContext(out AbstractBuilder<TResult> result)
{
ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func<BuilderContext, TResult>) }, null);

if (ctor != null)
{
return (AbstractBuilder<TResult>)ctor.Invoke(new object[] { _seedFunc });
result = (AbstractBuilder<TResult>)ctor.Invoke(new object[] { _seedFunc });
return true;
}

ctor = type.GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func<TResult>) }, null)
?? throw new MissingMethodException(GetType().Name, CtorConstants.MethodName);
result = null;
return false;
}

TResult SeedFuncWithoutCancellation() => _seedFunc(null);
/// <summary>
/// Attempts to create an instance of <see cref="AbstractBuilder{TResult}"/> using a constructor that accepts a <see cref="Func{TResult}"/>.
/// </summary>
/// <param name="type">The type of the builder.</param>
/// <param name="result">The created builder instance, if successful.</param>
/// <returns><c>true</c> if the builder was successfully created; otherwise, <c>false</c>.</returns>
private bool TryCreateBuilderWithFunction(out AbstractBuilder<TResult> result)
{
ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, new[] { typeof(Func<TResult>) }, null);

if (ctor != null)
{
TResult SeedFuncWithoutCancellation() => _seedFunc(null);

result = (AbstractBuilder<TResult>)ctor.Invoke(new object[] { (Func<TResult>)SeedFuncWithoutCancellation });
return true;
}

result = null;
return false;
}

/// <summary>
/// Attempts to create an instance of <see cref="AbstractBuilder{TResult}"/> using a parameterless constructor.
/// </summary>
/// <param name="type">The type of the builder.</param>
/// <param name="result">The created builder instance, if successful.</param>
/// <returns><c>true</c> if the builder was successfully created; otherwise, <c>false</c>.</returns>
private bool TryCreateBuilderWithDefaultCtor(out AbstractBuilder<TResult> result)
{
ConstructorInfo ctor = GetType().GetConstructor(CtorConstants.BindingFlags, null, Type.EmptyTypes, null);

if (ctor != null)
{
result = (AbstractBuilder<TResult>)ctor.Invoke(null);
return true;
}

return (AbstractBuilder<TResult>)ctor.Invoke(new object[] { (Func<TResult>)SeedFuncWithoutCancellation });
result = null;
return false;
}

/// <summary>
Expand All @@ -192,5 +264,14 @@ private bool IsSupported<TBuilder>()
Type currentType = GetType();
return resultType == currentType || resultType.IsInstanceOfType(currentType);
}

/// <summary>
/// Creates a default instance of <see cref="TResult"/>.
/// </summary>
/// <remarks>It is not used when a seed function is provided.</remarks>
protected virtual TResult CreateDefault()
{
return default;
}
}
}
6 changes: 3 additions & 3 deletions src/AbstractBuilder/AbstractBuilder.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<RepositoryUrl>https://github.com/p-caballero/AbstractBuilder</RepositoryUrl>
<RepositoryType>GIT</RepositoryType>
<Copyright>Copyright (c) 2020-2024 Pablo Caballero</Copyright>
<AssemblyVersion>1.6.0.0</AssemblyVersion>
<FileVersion>1.6.0.0</FileVersion>
<Version>1.6.0</Version>
<AssemblyVersion>1.7.0.0</AssemblyVersion>
<FileVersion>1.7.0.0</FileVersion>
<Version>1.7.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

Expand Down
15 changes: 11 additions & 4 deletions tests/UnitTests/AbstractBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MissingMethodException>(() =>
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]
Expand Down
Loading

0 comments on commit ef62f3c

Please sign in to comment.