Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b1fe4a4
docs: update nuget list (#194)
kamilbaczek Feb 12, 2025
09753e5
feat: replace FluentAssertions with Shouldly in tests
kamilbaczek Feb 11, 2025
463ba1c
feat: replace FluentAssertions with Shouldly in unit and integration …
kamilbaczek Feb 11, 2025
ae6ef2b
refactor: update assertions to use Shouldly in unit tests
Feb 13, 2025
e3cc23b
refactor: remove FluentAssertions global usings in favor of Shouldly
Feb 13, 2025
6d48b23
refactor: remove FluentAssertions package references in unit and inte…
Feb 13, 2025
4a05dda
docs: add architecture decision log for assertion framework selection
Feb 13, 2025
02e5579
docs: add architecture decision log for selecting Shouldly as asserti…
Feb 13, 2025
09fd197
refactor: add Shouldly global usings and remove unused references in …
Feb 13, 2025
f00d2cc
refactor: change return type to array for NewPassesRegistrationPerMon…
Feb 13, 2025
10c4228
refactor: fix spacing in return statement for NewPassesRegistrationPe…
Feb 13, 2025
76d79d5
refactor: remove unnecessary access modifier from Get method in IData…
kamilbaczek Feb 15, 2025
b89e3b0
docs: update architecture decision log
kamilbaczek Feb 15, 2025
ae94989
refactor: add Shouldly package and update assertions to use Shouldly
Mar 12, 2025
a98d98a
refactor: update tests to use Shouldly assertions instead of FluentAs…
Mar 12, 2025
8d37ca3
refactor: remove FluentAssertions global using and retain Shouldly
Mar 13, 2025
d0a07a1
refactor: remove unnecessary access modifier
Mar 13, 2025
490a56b
refactor: update return statement to use array spread syntax
Mar 13, 2025
317bfc3
refactor: update return statement to use array spread syntax
Mar 13, 2025
be886d5
refactor: update return statement to use array spread syntax with spa…
Mar 13, 2025
f523d01
feat: migrate from FluentAssertions to Shouldly in unit and integrati…
kamilbaczek Mar 16, 2025
01386bd
feat: replace FluentAssertions with Shouldly in unit tests
kamilbaczek Mar 16, 2025
da2ea55
docs: add decision document for migrating from Fluent Assertions to S…
kamilbaczek Mar 16, 2025
7bc650a
feat: migrate unit tests from FluentAssertions to Shouldly
kamilbaczek Mar 16, 2025
f42cded
feat: migrate unit tests from FluentAssertions to Shouldly
kamilbaczek Mar 16, 2025
bc8f17e
feat: update unit tests to use Shouldly assertions
kamilbaczek Mar 17, 2025
fde98c1
feat: add Shouldly global using and remove from ExceptionMiddlewareTests
kamilbaczek Mar 17, 2025
d7f0a83
feat: add Shouldly global using and remove local using from PrepareOf…
kamilbaczek Mar 17, 2025
c1eb01e
refactor: update return statement syntax and fix interface formatting
kamilbaczek Mar 18, 2025
73d0fdf
feat: migrate unit tests to use Shouldly assertions and clean up glob…
kamilbaczek Mar 18, 2025
b4ddd7d
refactor: update return statement to use array spread syntax in NewPa…
kamilbaczek Mar 18, 2025
aa6e450
refactor: remove unused global usings from GlobalUsings.cs
kamilbaczek Mar 18, 2025
90a1176
feat: add Shouldly package and update assertions in integration tests
kamilbaczek Mar 18, 2025
357e7fc
refactor: update type comparison in ValueObjectTests to use GetType m…
Mar 20, 2025
6924cd2
feat: improve images in chapter 2
kamilbaczek Mar 29, 2025
e5dd99c
Delete subdomains_architectural_patterns.jpg
kamilbaczek Mar 29, 2025
24c8f7c
Update README.adoc
kamilbaczek Mar 30, 2025
7f5571b
Update README.adoc
kamilbaczek Mar 30, 2025
cf190e9
Update README.adoc
kamilbaczek Mar 30, 2025
a5bae49
Update README.adoc
kamilbaczek Mar 30, 2025
ef17516
transform to md
kamilbaczek Mar 30, 2025
029f933
Update README.md
kamilbaczek Mar 30, 2025
b20a66b
feat: transform adoc
kamilbaczek Mar 30, 2025
4e4b9ef
Update README.md
kamilbaczek Mar 30, 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
Binary file removed Assets/subdomains_architectural_patterns.jpg
Binary file not shown.

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
4 changes: 2 additions & 2 deletions Chapter-2-modules-separation/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ Before we look at the technical solution, a few explanations are in order. First
- _Contracts_ is a _core_ type.
- The rest of our subdomains are still _supporting_.

image::Assets/subdomain_types.jpg[]
image::Assets/subdomain_types.png[]

The next step is to focus on the patterns that can be used in our modules - we were not able to make this decision in the first chapter due to lack of knowledge about our business domain and how it will be used by our customers (see _Project Paradox_ we described earlier). Thanks to the knowledge we gained, we decided on the following patterns for our modules:

- In _Contracts_ we will use the _Domain Model_
- For _Passes_ and _Offers_ we have chosen the _Active Record_
- In _Reports_ we will use _Transaction Script_

image::../Assets/subdomains_architectural_patterns.jpg[]
image::Assets/architectural_patterns.png[]

Let's translate all of the above into code.

Expand Down
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
Loading