Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Hafeed3s committed Dec 25, 2023
1 parent 67036c6 commit 150d140
Show file tree
Hide file tree
Showing 8 changed files with 457 additions and 0 deletions.
22 changes: 22 additions & 0 deletions SutForge.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SutForge", "src\SutForge\SutForge.csproj", "{89FD47F0-EC29-4DDE-AF20-239C5E6DA174}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SutForge.Tests", "test\SutForge.Tests\SutForge.Tests.csproj", "{1F6C7AA4-AE6F-42E7-8DBA-E348F6EA8E5A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{89FD47F0-EC29-4DDE-AF20-239C5E6DA174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89FD47F0-EC29-4DDE-AF20-239C5E6DA174}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89FD47F0-EC29-4DDE-AF20-239C5E6DA174}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89FD47F0-EC29-4DDE-AF20-239C5E6DA174}.Release|Any CPU.Build.0 = Release|Any CPU
{1F6C7AA4-AE6F-42E7-8DBA-E348F6EA8E5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F6C7AA4-AE6F-42E7-8DBA-E348F6EA8E5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F6C7AA4-AE6F-42E7-8DBA-E348F6EA8E5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F6C7AA4-AE6F-42E7-8DBA-E348F6EA8E5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
150 changes: 150 additions & 0 deletions src/SutForge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# SUT Forge

## Introduction

Experience unit testing and Test-Driven Development (TDD) in a whole new light with `SUTForge`, a simple yet powerful .NET package meticulously designed to streamline the creation of System Under Test (SUT) instances. `SUTForge` follows the Chicago School to unit testing (also referred to as the Classical School of unit testing), offering a clean and efficient alternative to the over-mocking issues associated with the London approach. What sets `SUTForge` apart is its high degree of extensibility, allowing developers to craft personalized extension methods and tailor SUTs to their specific testing needs.



**Key Features**:

- **Effortless TDD Integration**: `SUTForge` seamlessly integrates with Test-Driven Development (TDD), enabling developers to write tests first and then effortlessly construct and configure their SUTs using the intuitive builder pattern provided by `SUTForge`.

- **Classical Approach to Unit Testing**: Embrace the classical approach to unit testing, emphasizing simplicity and readability. `SUTForge` encourages clean and maintainable tests without succumbing to over-mocking pitfalls, ensuring your unit tests remain valuable assets throughout the development lifecycle.

- **Intuitive Builder Pattern**: Constructing SUTs becomes a breeze with `SUTForge`'s intuitive builder pattern. Say goodbye to the complexities of manual setup and embrace a syntax that makes unit test code concise, expressive, and easy to understand.

- **Highly Extensible**: `SUTForge` goes beyond the basics, offering developers a high degree of extensibility. Write your own extension methods to customize SUTs according to your unique testing requirements. Tailor `SUTForge` to fit seamlessly into your specific development workflow.

💪 Elevate your unit testing and TDD experience with `SUTForge`—a library that not only embraces the classical approach but also empowers you to extend and customize your testing capabilities. Your journey to efficient and maintainable tests starts here!

## Getting Started

To get started with `SUTForge`, you need to install the package in your .NET project. Once installed, you can start writing your unit tests using the `SUTForge`'s intuitive builder pattern.

### Installation

Install `SUTForge` via NuGet package manager:

```shell
Install-Package SUTForge
```

### Writing Your First Test

Here's an example of how you can write a unit test using `SUTForge`. This test checks whether a service is correctly built by the builder.

```csharp
[Test]
public async Task Services_are_built_by_the_builder()
{
// Arrange
// Act
var sut = await SutBuilder.Create
.ConfigureServices(services => { services.AddSingleton<ISomeInterface, SomeImplementation>(); })
.BuildAsync();

// Assert
sut.Should().NotBeNull();
sut.GetService<ISomeInterface>().Should().NotBeNull().And.BeOfType<SomeImplementation>();
}
```

In the above test, we're using the `SutBuilder.Create` method to create a new instance of the SUT builder. We then configure the services by adding a singleton service of type `ISomeInterface` with an implementation of `SomeImplementation`. After building the SUT, we assert that the SUT is not null and that the service of type `ISomeInterface` is not null and is of type `SomeImplementation`.

### Resolving Configuration for Services

`SUTForge` also allows you to resolve configurations for services. Here's an example:

```csharp
[Test]
public async Task Configuration_is_resolved_for_services()
{
// Arrange
// Act
var sut = await SutBuilder.Create
.ConfigureServices(services => { services.AddSingleton<ClassWithConfiguration>(); })
.BuildAsync();

// Assert
sut.GetService<ClassWithConfiguration>().Should().NotBeNull();
}
```

In this test, we're adding a service of type `ClassWithConfiguration` that requires configuration. After building the SUT, we assert that the service of type `ClassWithConfiguration` is not null, implying that the configuration was resolved correctly.

### Customizing Configuration

`SUTForge` also allows you to customize the configuration. Here's an example:

```csharp
[Test]
public async Task Configuration_can_be_customized()
{
// Arrange
// Act
var sut = await SutBuilder.Create
.ConfigureServices(services => { services.AddSingleton<ClassWithConfiguration>(); })
.OnSetupConfiguration(builder => builder.AddInMemoryCollection(new Dictionary<string, string>
{
{"Country", "France"}
}))
.BuildAsync();

// Assert
var service = sut.GetService<ClassWithConfiguration>();
service.Should().NotBeNull();
service.Configuration["Country"].Should().Be("France");
}
```

In this test, we're customizing the configuration by adding an in-memory collection with a key-value pair of "Country" and "France". After building the SUT, we assert that the service of type `ClassWithConfiguration` is not null and that the configuration value for "Country" is "France".

### Extending SUTForge

`SUTForge` is highly extensible, allowing you to write your own extension methods to customize SUTs according to your unique testing requirements. Here's an example:

```csharp

public static class SutBuilderExtensions
{
public static SutBuilder WithCurrentTime(this SutBuilder builder, DateTime time)
{
return builder.ConfigureServices(services => { services.AddSingleton<ITimeProvider(new DeterministicTimeProvider(time)); });
}
}
```


In this example, we're writing an extension method that allows us to customize the SUT by adding a singleton service of type `ITimeProvider` with an implementation of `DeterministicTimeProvider`. This extension method can then be used in our tests as follows:

```csharp
[Test]
public async Task SUT_can_be_customized()
{
// Arrange
var time = 14.January(2021).At(12, 0, 0).AsUtc(); // Using FluentAssertions
var sut = await SutBuilder.Create
.AddApplicationServices() // Add the application services
.WithCurrentTime(time)
.BuildAsync();

var service = sut.GetRequiredService<ISomeFancyService>();

// Act
var result = service.DoSomething();

// Assert
// Assert something
}
```

With `SUTForge`, writing unit tests becomes a breeze. Embrace the classical approach to unit testing and elevate your TDD experience with `SUTForge`. Happy testing!

## References

- [Unit Testing Principles, Practices, and Patterns](https://www.goodreads.com/en/book/show/48927138) (Vladimir Khorikov)
- [🚀 TDD, Where Did It All Go Wrong (Ian Cooper)](https://www.youtube.com/watch?v=EZ05e7EMOLM)
- Uncle Bob (Robert C. Martin), TDD Harms Architecture https://blog.cleancoder.com/uncle-bob/2017/03/03/TDD-Harms-Architecture.html (On a shift from London to Chicago School of TDD; About fallacy of <class>Test file pattern)
- [TDD Revisited - Ian Cooper - NDC London 2021](https://www.youtube.com/watch?v=vOO3hulIcsY) (on testing “ports&adapters” architecture with Chicago School of TDD)
20 changes: 20 additions & 0 deletions src/SutForge/Sut.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace SutForge;

/// <summary>
/// Represents a System Under Test. This SUT implements <see cref="IServiceProvider"/> to allow to retrieve services from the <see cref="IServiceProvider"/> used to build the SUT.
/// </summary>
public class Sut : IServiceProvider
{
private IServiceProvider _serviceProviderImplementation;

internal Sut(IServiceProvider serviceProviderImplementation)
{
_serviceProviderImplementation = serviceProviderImplementation;
}

/// <inheritdoc />
public object? GetService(Type serviceType)
{
return _serviceProviderImplementation.GetService(serviceType);
}
}
79 changes: 79 additions & 0 deletions src/SutForge/SutBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace SutForge;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Allows to build a <see cref="Sut"/> (System Under Test) with a custom configuration.
/// </summary>
public class SutBuilder
{
private Action<IServiceCollection> configureServices = _ => { };
private Action<ConfigurationBuilder> configurationBuilders = _ => { };
private Func<Sut, Task> onCreated = _ => Task.CompletedTask;

protected SutBuilder()
{
}

public static SutBuilder Create => new();

/// <summary>
/// Builds the <see cref="Sut"/> (System Under Test) with the provided configuration.
/// </summary>
/// <returns>An instance of <see cref="Sut"/> ready to be tested</returns>
public async Task<Sut> BuildAsync()
{
// Create the empty service collection and the configuration
var services = new ServiceCollection();
var configurationBuilder = new ConfigurationBuilder();

// Execute the registered configuration builders
this.configurationBuilders(configurationBuilder);

// Add the configuration to the service collection
services.AddSingleton<IConfiguration>(configurationBuilder.Build());

// Execute the registered services configuration
this.configureServices(services);

// Build the service provider
var serviceProvider = services.BuildServiceProvider();

// Create the SUT and execute the registered operations
var sut = new Sut(serviceProvider);
await this.onCreated(sut);

return sut;
}

/// <summary>
/// Registers an action to configure the services of the <see cref="Sut"/> (System Under Test).
/// </summary>
public SutBuilder ConfigureServices(Action<IServiceCollection> configureServices)
{
this.configureServices += configureServices;

return this;
}

/// <summary>
/// Registers an action to configure a <see cref="IConfiguration"/> which will be passed to the <see cref="OnCreatedAsync"/> methods.
/// </summary>
public SutBuilder OnSetupConfiguration(Action<ConfigurationBuilder> builder)
{
this.configurationBuilders += builder;

return this;
}

/// <summary>
/// Registers an action to execute when the <see cref="Sut"/> (System Under Test) is created.
/// </summary>
public SutBuilder OnCreatedAsync(Func<Sut, Task> operation)
{
this.onCreated += operation;

return this;
}
}
15 changes: 15 additions & 0 deletions src/SutForge/SutForge.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Sutb</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions test/SutForge.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using NUnit.Framework;
global using FluentAssertions;
Loading

0 comments on commit 150d140

Please sign in to comment.