Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
02b9075
feat: replace FluentAssertions with Shouldly in tests
kamilbaczek Feb 11, 2025
2e6f6c8
feat: replace FluentAssertions with Shouldly in unit and integration …
kamilbaczek Feb 11, 2025
6b9fd20
refactor: update assertions to use Shouldly in unit tests
Feb 13, 2025
42db985
refactor: remove FluentAssertions global usings in favor of Shouldly
Feb 13, 2025
263f0f0
refactor: remove FluentAssertions package references in unit and inte…
Feb 13, 2025
381fa05
docs: add architecture decision log for assertion framework selection
Feb 13, 2025
7da4dfb
docs: add architecture decision log for selecting Shouldly as asserti…
Feb 13, 2025
1442867
refactor: add Shouldly global usings and remove unused references in …
Feb 13, 2025
f908d69
refactor: change return type to array for NewPassesRegistrationPerMon…
Feb 13, 2025
c98ba84
refactor: fix spacing in return statement for NewPassesRegistrationPe…
Feb 13, 2025
0fc9780
refactor: remove unnecessary access modifier from Get method in IData…
kamilbaczek Feb 15, 2025
2a8ac64
docs: update architecture decision log
kamilbaczek Feb 15, 2025
8c38bd5
feat: replace FluentAssertions with Shouldly in tests
kamilbaczek Feb 11, 2025
c6212a0
feat: replace FluentAssertions with Shouldly in unit and integration …
kamilbaczek Feb 11, 2025
4fbd830
refactor: update assertions to use Shouldly in unit tests
Feb 13, 2025
69622fa
refactor: remove FluentAssertions global usings in favor of Shouldly
Feb 13, 2025
62bca24
refactor: remove FluentAssertions package references in unit and inte…
Feb 13, 2025
ee3666d
docs: add architecture decision log for assertion framework selection
Feb 13, 2025
9aab911
docs: add architecture decision log for selecting Shouldly as asserti…
Feb 13, 2025
9c4204c
refactor: add Shouldly global usings and remove unused references in …
Feb 13, 2025
eb38998
refactor: change return type to array for NewPassesRegistrationPerMon…
Feb 13, 2025
da1f5ff
refactor: fix spacing in return statement for NewPassesRegistrationPe…
Feb 13, 2025
bc0d84e
refactor: remove unnecessary access modifier from Get method in IData…
kamilbaczek Feb 15, 2025
6e95296
docs: update architecture decision log
kamilbaczek Feb 15, 2025
d804e77
docs: update adr formatting
kamilbaczek Feb 15, 2025
4a1c1fc
Merge branch 'feature/chapter-2-migration-to-shoudly' of https://gith…
kamilbaczek Feb 15, 2025
2639900
Revert "docs: update adr formatting"
kamilbaczek Feb 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
= 18. Select Fluent Assertion Alternative Due to Licensing Issue

Date: 2025-09-04

== Problem

We have been using Fluent Assertions as an assertion library to enhance the readability and maintainability of our test code through fluent interfaces. However, starting from version 8, Fluent Assertions has transitioned to a paid NuGet package. Given the current pricing model, the cost outweighs the benefits it provides to our project. Consequently, we are evaluating open-source alternatives to replace Fluent Assertions.

=== Option 1: NFluentAssertions

This library is a fork of Fluent Assertions, allowing us to transition without modifying our existing assertions—only a NuGet package change is required.

The project is maintained by a separate group of developers, raising concerns about its long-term stability and ongoing support.

Repository: https://github.com/tpierrain/NFluent

=== Option 2: MSTest Assertions

These assertions are the standard Microsoft .NET testing utilities, maintained and updated with each .NET release.

They offer a reliable and well-supported alternative but lack the fluent syntax, resulting in assertions that are less readable and more verbose.

Documentation: https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest

=== Option 3: Shouldly

Shouldly provides a similar fluent interface to Fluent Assertions with minor syntactic differences.

It is widely supported, maintained by an active community of contributors, and backed by sponsors, ensuring its long-term viability.

The primary downside is the need to refactor existing assertions to align with Shouldly’s syntax.

Repository: https://github.com/shouldly/shouldly

== Decision

After a thorough analysis of available options, we have decided to migrate from Fluent Assertions to Shouldly.

== Consequences
- Improved Long-Term Stability: Shouldly is actively maintained and supported by the open-source community, reducing the risk of unexpected licensing changes.
- Maintainability & Readability: Shouldly retains a fluent syntax, ensuring our tests remain easy to read and maintain.
- Refactoring Effort: Transitioning to Shouldly requires refactoring existing assertions, introducing short-term overhead but ensuring long-term sustainability.
- Cost Reduction: Moving away from a paid assertion library eliminates unnecessary expenditure while maintaining similar functionality.
- Ecosystem Alignment: Shouldly is widely adopted in the .NET ecosystem, ensuring compatibility and integration with modern development workflows.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
<PackageReference Include="Verify.Xunit" Version="28.3.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace EvolutionaryArchitecture.Fitnet.Common.IntegrationTests.TestEngine.Dat

public interface IDatabaseConfiguration
{
public Dictionary<string, string?> Get();
}
Dictionary<string, string?> Get();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Update="SonarAnalyzer.CSharp" Version="9.32.0.97167" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ internal async Task Given_business_rule_validation_exception_Then_returns_confli
await exceptionHandler.TryHandleAsync(_context, new BusinessRuleValidationException(exceptionMessage), default);

// Assert
_context.Response.StatusCode.Should().Be((int)HttpStatusCode.Conflict);
_context.Response.StatusCode.ShouldBe((int)HttpStatusCode.Conflict);

var responseMessage = await GetExceptionResponseMessage();
responseMessage.Title.Should().Be(exceptionMessage);
responseMessage.Title.ShouldBe(exceptionMessage);
}

[Fact]
Expand All @@ -40,10 +40,10 @@ internal async Task Given_other_than_business_rule_validation_exception_Then_ret
await exceptionHandler.TryHandleAsync(_context, new InvalidCastException("test"), CancellationToken.None);

// Assert
_context.Response.StatusCode.Should().Be((int)HttpStatusCode.InternalServerError);
_context.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError);

var responseMessage = await GetExceptionResponseMessage();
responseMessage.Title.Should().Be(exceptionMessage);
responseMessage.Title.ShouldBe(exceptionMessage);
}

private static DefaultHttpContext GetHttpContext() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
global using System.Net;
global using Xunit;
global using FluentAssertions;
global using Microsoft.AspNetCore.Http;
global using Newtonsoft.Json;
global using NSubstitute;
global using NSubstitute;
global using Shouldly;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal void Given_concrete_business_rule_which_is_met_Then_validation_should_p
var act = () => BusinessRuleValidator.Validate(new FakeBusinessRule(20));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}

[Fact]
Expand All @@ -25,6 +25,7 @@ internal void Given_concrete_business_rule_which_is_not_met_Then_validation_shou
var act = () => BusinessRuleValidator.Validate(new FakeBusinessRule(1));

// Assert
act.Should().Throw<BusinessRuleValidationException>().WithMessage("Fake business rule was not met");
var exception = act.ShouldThrow<BusinessRuleValidationException>();
exception.Message.ShouldBe("Fake business rule was not met");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Update="SonarAnalyzer.CSharp" Version="9.32.0.97167" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
global using Xunit;
global using FluentAssertions;
global using Shouldly;
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal async Task Given_valid_event_published_Then_event_should_be_consumed()
await eventBus!.PublishAsync(fakeEvent, CancellationToken.None);

// Assert
fakeEvent.Consumed.Should().BeTrue();
fakeEvent.Consumed.ShouldBeTrue();
}

private IEventBus GetEventBus() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Update="SonarAnalyzer.CSharp" Version="9.32.0.97167" />
<PackageReference Include="Shouldly" Version="4.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
global using System.Reflection;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.AspNetCore.Mvc.Testing;
global using Shouldly;
global using Xunit;
global using FluentAssertions;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.analyzers" Version="1.17.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
global using System.Collections;
global using FluentAssertions;
global using Xunit;
global using Shouldly;
global using Xunit;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ internal void Given_customer_age_which_is_less_than_18_Then_validation_should_th
var act = () => BusinessRuleValidator.Validate(new ContractCanBePreparedOnlyForAdultRule(17));

// Assert
act.Should().Throw<BusinessRuleValidationException>().WithMessage("Contract can not be prepared for a person who is not adult");
var exception = act.ShouldThrow<BusinessRuleValidationException>();
exception.Message.ShouldBe("Contract can not be prepared for a person who is not adult");
}

[Fact]
Expand All @@ -26,7 +27,7 @@ internal void Given_customer_age_which_is_equal_to_18_Then_validation_should_pas
var act = () => BusinessRuleValidator.Validate(new ContractCanBePreparedOnlyForAdultRule(18));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}

[Fact]
Expand All @@ -38,6 +39,6 @@ internal void Given_customer_age_which_is_greater_than_18_Then_validation_should
var act = () => BusinessRuleValidator.Validate(new ContractCanBePreparedOnlyForAdultRule(19));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ internal void Given_customer_height_which_is_greater_than_maximum_height_limit_T
var act = () => BusinessRuleValidator.Validate(new CustomerMustBeSmallerThanMaximumHeightLimitRule(height));

// Assert
act.Should().Throw<BusinessRuleValidationException>().WithMessage("Customer height must fit maximum limit for gym instruments");
var exception = act.ShouldThrow<BusinessRuleValidationException>();
exception.Message.ShouldBe("Customer height must fit maximum limit for gym instruments");
}

[Fact]
Expand All @@ -28,7 +29,7 @@ internal void Given_customer_height_which_is_equal_to_maximum_height_limit_Then_
var act = () => BusinessRuleValidator.Validate(new CustomerMustBeSmallerThanMaximumHeightLimitRule(height));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}

[Fact]
Expand All @@ -41,6 +42,6 @@ internal void Given_customer_height_which_is_less_than_maximum_height_limit_Then
var act = () => BusinessRuleValidator.Validate(new CustomerMustBeSmallerThanMaximumHeightLimitRule(height));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal void Given_previous_contract_signed_Then_validation_should_pass()
var act = () => BusinessRuleValidator.Validate(new PreviousContractHasToBeSignedRule(true));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}

[Fact]
Expand All @@ -26,10 +26,9 @@ internal void Given_previous_contract_not_exists_Then_validation_should_pass()
var act = () => BusinessRuleValidator.Validate(new PreviousContractHasToBeSignedRule(null));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}


[Fact]
internal void Given_previous_contract_unsigned_Then_validation_should_throw()
{
Expand All @@ -39,6 +38,7 @@ internal void Given_previous_contract_unsigned_Then_validation_should_throw()
var act = () => BusinessRuleValidator.Validate(new PreviousContractHasToBeSignedRule(false));

// Assert
act.Should().Throw<BusinessRuleValidationException>().WithMessage("Previous contract must be signed by the customer");
var exception = act.ShouldThrow<BusinessRuleValidationException>();
exception.Message.ShouldBe("Previous contract must be signed by the customer");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ internal void Given_signed_at_date_which_is_more_than_30_days_from_prepared_at_d
DateTimeOffset.Now.AddDays(31)));

// Assert
act.Should().Throw<BusinessRuleValidationException>().WithMessage(
"Contract can not be signed because more than 30 days have passed from the contract preparation");
var exception = act.ShouldThrow<BusinessRuleValidationException>();
exception.Message.ShouldBe("Contract can not be signed because more than 30 days have passed from the contract preparation");
}

[Fact]
Expand All @@ -33,7 +33,7 @@ internal void Given_signed_at_date_which_is_30_days_from_prepared_at_date_Then_v
DateTimeOffset.Now.AddDays(30)));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}

[Fact]
Expand All @@ -48,6 +48,6 @@ internal void Given_signed_at_date_which_is_less_than_30_days_from_prepared_at_d
DateTimeOffset.Now.AddDays(29)));

// Assert
act.Should().NotThrow<BusinessRuleValidationException>();
act.ShouldNotThrow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal void Given_sign_contract_Then_expiration_date_is_set_to_contract_durati
contract.Sign(signedAt, fakeNow);

// Assert
contract.ExpiringAt.Should().Be(expectedExpirationDate);
contract.ExpiringAt.ShouldBe(expectedExpirationDate);
}

private static Contract PrepareContract(DateTimeOffset preparedAt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Update="SonarAnalyzer.CSharp" Version="9.32.0.97167" />
<PackageReference Include="Shouldly" Version="4.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
global using System.Net.Http.Json;
global using Xunit;
global using Bogus;
global using FluentAssertions;
global using NSubstitute;
global using NSubstitute;
global using Shouldly;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal async Task Given_valid_contract_preparation_request_Then_should_return_
var prepareContractResponse = await PrepareCorrectContract(requestParameters);

// Assert
prepareContractResponse.Should().HaveStatusCode(HttpStatusCode.Created);
prepareContractResponse.StatusCode.ShouldBe(HttpStatusCode.Created);
}

[Fact]
Expand All @@ -44,9 +44,8 @@ internal async Task Given_contract_preparation_request_with_invalid_age_Then_sho

// Assert
var responseMessage = await prepareContractResponse.Content.ReadFromJsonAsync<ProblemDetails>();
responseMessage?.Status.Should().Be((int)HttpStatusCode.Conflict);
responseMessage?.Title.Should()
.Be("Contract can not be prepared for a person who is not adult");
responseMessage?.Status.ShouldBe((int)HttpStatusCode.Conflict);
responseMessage?.Title.ShouldBe("Contract can not be prepared for a person who is not adult");
}

[Fact]
Expand All @@ -64,9 +63,8 @@ internal async Task Given_contract_preparation_request_with_invalid_height_Then_

// Assert
var responseMessage = await prepareContractResponse.Content.ReadFromJsonAsync<ProblemDetails>();
responseMessage?.Status.Should().Be((int)HttpStatusCode.Conflict);
responseMessage?.Title.Should()
.Be("Customer height must fit maximum limit for gym instruments");
responseMessage?.Status.ShouldBe((int)HttpStatusCode.Conflict);
responseMessage?.Title.ShouldBe("Customer height must fit maximum limit for gym instruments");
}

[Fact]
Expand All @@ -82,9 +80,8 @@ internal async Task Given_contract_preparation_request_When_contract_for_custome

// Assert
var responseMessage = await prepareContractResponse.Content.ReadFromJsonAsync<ProblemDetails>();
responseMessage?.Status.Should().Be((int)HttpStatusCode.Conflict);
responseMessage?.Title.Should()
.Be("Previous contract must be signed by the customer");
responseMessage?.Status.ShouldBe((int)HttpStatusCode.Conflict);
responseMessage?.Title.ShouldBe("Previous contract must be signed by the customer");
}

private async Task<HttpResponseMessage> PrepareCorrectContract(PrepareContractRequestParameters requestParameters, Guid? customerId = null)
Expand Down
Loading