From 39aba7f38a2a550d8d82082437ad905091110a1e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 18 Nov 2024 06:44:34 +1000 Subject: [PATCH 01/10] Converted Domain tests to TUnit --- tests/Directory.Packages.props | 1 + .../Domain.UnitTests/Domain.UnitTests.csproj | 13 +++++----- tests/Domain.UnitTests/Heroes/HeroTests.cs | 26 +++++++++---------- tests/Domain.UnitTests/Heroes/PowerTests.cs | 18 ++++++------- tests/Domain.UnitTests/Teams/MissionTests.cs | 6 ++--- tests/Domain.UnitTests/Teams/TeamTests.cs | 24 ++++++++--------- tests/Domain.UnitTests/Usings.cs | 2 +- 7 files changed, 46 insertions(+), 44 deletions(-) diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 77891684..10415c45 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -7,6 +7,7 @@ + all diff --git a/tests/Domain.UnitTests/Domain.UnitTests.csproj b/tests/Domain.UnitTests/Domain.UnitTests.csproj index 21fc7ab2..79890f86 100644 --- a/tests/Domain.UnitTests/Domain.UnitTests.csproj +++ b/tests/Domain.UnitTests/Domain.UnitTests.csproj @@ -12,12 +12,13 @@ - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + + + + diff --git a/tests/Domain.UnitTests/Heroes/HeroTests.cs b/tests/Domain.UnitTests/Heroes/HeroTests.cs index 64f69bc6..29634f88 100644 --- a/tests/Domain.UnitTests/Heroes/HeroTests.cs +++ b/tests/Domain.UnitTests/Heroes/HeroTests.cs @@ -4,9 +4,9 @@ namespace SSW.CleanArchitecture.Domain.UnitTests.Heroes; public class HeroTests { - [Theory] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] + [Test] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] public void HeroId_ShouldBeComparable(string stringGuid1, string stringGuid2, bool isEqual) { // Arrange @@ -24,7 +24,7 @@ public void HeroId_ShouldBeComparable(string stringGuid1, string stringGuid2, bo id2.Value.Should().Be(guid2); } - [Fact] + [Test] public void Create_WithValidNameAndAlias_ShouldSucceed() { // Arrange @@ -40,7 +40,7 @@ public void Create_WithValidNameAndAlias_ShouldSucceed() hero.Alias.Should().Be(alias); } - [Fact] + [Test] public void Create_WithSameNameAndAlias_ShouldSucceed() { // Arrange @@ -51,10 +51,10 @@ public void Create_WithSameNameAndAlias_ShouldSucceed() Hero.Create(name, alias); } - [Theory] - [InlineData(null, "alias")] - [InlineData("name", null)] - [InlineData(null, null)] + [Test] + [Arguments(null, "alias")] + [Arguments("name", null)] + [Arguments(null, null)] public void Create_WithNullTitleOrAlias_ShouldThrow(string? name, string? alias) { // Arrange @@ -66,7 +66,7 @@ public void Create_WithNullTitleOrAlias_ShouldThrow(string? name, string? alias) act.Should().Throw().WithMessage("Value cannot be null*"); } - [Fact] + [Test] public void AddPower_ShouldUpdateHeroPowerLevel() { // Act @@ -79,7 +79,7 @@ public void AddPower_ShouldUpdateHeroPowerLevel() hero.Powers.Should().HaveCount(2); } - [Fact] + [Test] public void RemovePower_ShouldUpdateHeroPowerLevel() { // Act @@ -95,7 +95,7 @@ public void RemovePower_ShouldUpdateHeroPowerLevel() hero.Powers.Should().HaveCount(1); } - [Fact] + [Test] public void AddPower_ShouldRaisePowerLevelUpdatedEvent() { // Act @@ -117,7 +117,7 @@ public void AddPower_ShouldRaisePowerLevelUpdatedEvent() hero.Powers.Should().ContainSingle("Super-strength"); } - [Fact] + [Test] public void RemovePower_ShouldRaisePowerLevelUpdatedEvent() { // Act diff --git a/tests/Domain.UnitTests/Heroes/PowerTests.cs b/tests/Domain.UnitTests/Heroes/PowerTests.cs index be5adcd8..8a927327 100644 --- a/tests/Domain.UnitTests/Heroes/PowerTests.cs +++ b/tests/Domain.UnitTests/Heroes/PowerTests.cs @@ -4,7 +4,7 @@ namespace SSW.CleanArchitecture.Domain.UnitTests.Heroes; public class PowerTests { - [Fact] + [Test] public void Power_ShouldBeCreatable() { // Arrange @@ -20,7 +20,7 @@ public void Power_ShouldBeCreatable() power.PowerLevel.Should().Be(powerLevel); } - [Fact] + [Test] public void Power_ShouldBeComparable() { // Arrange @@ -36,13 +36,13 @@ public void Power_ShouldBeComparable() areEqual.Should().BeTrue(); } - [Theory] - [InlineData(-1, true)] - [InlineData(0, true)] - [InlineData(1, false)] - [InlineData(9, false)] - [InlineData(10, false)] - [InlineData(11, true)] + [Test] + [Arguments(-1, true)] + [Arguments(0, true)] + [Arguments(1, false)] + [Arguments(9, false)] + [Arguments(10, false)] + [Arguments(11, true)] public void Power_WithInvalidPowerLevel_ShouldThrow(int powerLevel, bool shouldThrow) { // Arrange diff --git a/tests/Domain.UnitTests/Teams/MissionTests.cs b/tests/Domain.UnitTests/Teams/MissionTests.cs index c9211bee..05be821d 100644 --- a/tests/Domain.UnitTests/Teams/MissionTests.cs +++ b/tests/Domain.UnitTests/Teams/MissionTests.cs @@ -4,9 +4,9 @@ namespace SSW.CleanArchitecture.Domain.UnitTests.Teams; public class MissionTests { - [Theory] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] + [Test] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] public void MissionId_ShouldBeComparable(string stringGuid1, string stringGuid2, bool isEqual) { // Arrange diff --git a/tests/Domain.UnitTests/Teams/TeamTests.cs b/tests/Domain.UnitTests/Teams/TeamTests.cs index 77134d2f..9bc0dabc 100644 --- a/tests/Domain.UnitTests/Teams/TeamTests.cs +++ b/tests/Domain.UnitTests/Teams/TeamTests.cs @@ -5,9 +5,9 @@ namespace SSW.CleanArchitecture.Domain.UnitTests.Teams; public class TeamTests { - [Theory] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] - [InlineData("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] + [Test] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] + [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] public void TeamId_ShouldBeComparable(string stringGuid1, string stringGuid2, bool isEqual) { // Arrange @@ -25,7 +25,7 @@ public void TeamId_ShouldBeComparable(string stringGuid1, string stringGuid2, bo id2.Value.Should().Be(guid2); } - [Fact] + [Test] public void Create_WithValidNameAndAlias_ShouldSucceed() { // Arrange @@ -39,7 +39,7 @@ public void Create_WithValidNameAndAlias_ShouldSucceed() team.Name.Should().Be(name); } - [Fact] + [Test] public void Create_WithNullNameAndAlias_ShouldThrow() { // Arrange @@ -52,7 +52,7 @@ public void Create_WithNullNameAndAlias_ShouldThrow() act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'name')"); } - [Fact] + [Test] public void AddHero_ShouldUpdateTeamPowerLevel() { // Arrange @@ -72,7 +72,7 @@ public void AddHero_ShouldUpdateTeamPowerLevel() team.TotalPowerLevel.Should().Be(14); } - [Fact] + [Test] public void RemoveHero_ShouldUpdateTeamPowerLevel() { // Arrange @@ -93,7 +93,7 @@ public void RemoveHero_ShouldUpdateTeamPowerLevel() team.TotalPowerLevel.Should().Be(4); } - [Fact] + [Test] public void ExecuteMission_ShouldUpdateTeamStatus() { // Arrange @@ -108,7 +108,7 @@ public void ExecuteMission_ShouldUpdateTeamStatus() team.Missions.Should().ContainSingle(x => x.Description == "Mission"); } - [Fact] + [Test] public void ExecuteMission_WhenTeamNotAvailable_ShouldError() { // Arrange @@ -123,7 +123,7 @@ public void ExecuteMission_WhenTeamNotAvailable_ShouldError() result.FirstError.Should().Be(TeamErrors.NotAvailable); } - [Fact] + [Test] public void CompleteCurrentMission_ShouldUpdateTeamStatus() { // Arrange @@ -137,7 +137,7 @@ public void CompleteCurrentMission_ShouldUpdateTeamStatus() team.Status.Should().Be(TeamStatus.Available); } - [Fact] + [Test] public void CompleteCurrentMission_WhenNoMissionHasBeenExecuted_ShouldThrow() { // Arrange @@ -151,7 +151,7 @@ public void CompleteCurrentMission_WhenNoMissionHasBeenExecuted_ShouldThrow() result.FirstError.Should().Be(TeamErrors.NotOnMission); } - [Fact] + [Test] public void CompleteCurrentMission_WhenNotOnMission_ShouldError() { // Arrange diff --git a/tests/Domain.UnitTests/Usings.cs b/tests/Domain.UnitTests/Usings.cs index 7fef4b0e..62b52c3a 100644 --- a/tests/Domain.UnitTests/Usings.cs +++ b/tests/Domain.UnitTests/Usings.cs @@ -1,2 +1,2 @@ -global using Xunit; +global using TUnit; global using FluentAssertions; \ No newline at end of file From 71dfe4e8091ea0f4639d11a9da8bb6035a855807 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 18 Nov 2024 06:52:20 +1000 Subject: [PATCH 02/10] Converted arch tests to TUnit --- tests/Architecture.Tests/Application.cs | 4 +-- .../Architecture.Tests.csproj | 14 +++++----- .../Common/TestResultAssertions.cs | 1 + .../Common/TestResultExtensions.cs | 27 ++++++++++--------- .../Common/TypeExtensions.cs | 25 ++++++++--------- tests/Architecture.Tests/DomainTests.cs | 4 +-- tests/Architecture.Tests/Layer.cs | 8 +++--- tests/Architecture.Tests/Presentation.cs | 2 +- tests/Architecture.Tests/Usings.cs | 2 +- 9 files changed, 46 insertions(+), 41 deletions(-) diff --git a/tests/Architecture.Tests/Application.cs b/tests/Architecture.Tests/Application.cs index 0cc8f2ac..dbcdcfbe 100644 --- a/tests/Architecture.Tests/Application.cs +++ b/tests/Architecture.Tests/Application.cs @@ -7,7 +7,7 @@ public class Application : TestBase { private static readonly Type IRequestHandler = typeof(IRequestHandler<,>); - [Fact] + [Test] public void CommandHandlers_ShouldHaveCorrectSuffix() { var types = Types @@ -25,7 +25,7 @@ public void CommandHandlers_ShouldHaveCorrectSuffix() result.Should().BeSuccessful(); } - [Fact] + [Test] public void QueryHandlers_ShouldHaveCorrectSuffix() { var types = Types diff --git a/tests/Architecture.Tests/Architecture.Tests.csproj b/tests/Architecture.Tests/Architecture.Tests.csproj index 0f85732f..dcc14643 100644 --- a/tests/Architecture.Tests/Architecture.Tests.csproj +++ b/tests/Architecture.Tests/Architecture.Tests.csproj @@ -12,13 +12,15 @@ - + - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + + + + diff --git a/tests/Architecture.Tests/Common/TestResultAssertions.cs b/tests/Architecture.Tests/Common/TestResultAssertions.cs index c49956d7..3b938ff7 100644 --- a/tests/Architecture.Tests/Common/TestResultAssertions.cs +++ b/tests/Architecture.Tests/Common/TestResultAssertions.cs @@ -1,6 +1,7 @@ using FluentAssertions.Execution; using FluentAssertions.Primitives; using System.Text; +using TestResult = NetArchTest.Rules.TestResult; namespace SSW.CleanArchitecture.Architecture.UnitTests.Common; diff --git a/tests/Architecture.Tests/Common/TestResultExtensions.cs b/tests/Architecture.Tests/Common/TestResultExtensions.cs index 767fa0f6..3432826c 100644 --- a/tests/Architecture.Tests/Common/TestResultExtensions.cs +++ b/tests/Architecture.Tests/Common/TestResultExtensions.cs @@ -1,19 +1,20 @@ -using Xunit.Abstractions; +// using Xunit.Abstractions; +using TestResult = NetArchTest.Rules.TestResult; namespace SSW.CleanArchitecture.Architecture.UnitTests.Common; - public static class TestResultExtensions { - public static void DumpFailingTypes(this TestResult result, ITestOutputHelper outputHelper) - { - if (result.IsSuccessful) - return; - - outputHelper.WriteLine("Failing Types:"); - - foreach (var type in result.FailingTypes) - outputHelper.WriteLine(type.FullName); - } - +// TODO: Fix up +// public static void DumpFailingTypes(this TestResult result, ITestOutputHelper outputHelper) +// { +// if (result.IsSuccessful) +// return; +// +// outputHelper.WriteLine("Failing Types:"); +// +// foreach (var type in result.FailingTypes) +// outputHelper.WriteLine(type.FullName); +// } +// public static TestResultAssertions Should(this TestResult result) => new(result); } \ No newline at end of file diff --git a/tests/Architecture.Tests/Common/TypeExtensions.cs b/tests/Architecture.Tests/Common/TypeExtensions.cs index 9406f542..5aa1da6e 100644 --- a/tests/Architecture.Tests/Common/TypeExtensions.cs +++ b/tests/Architecture.Tests/Common/TypeExtensions.cs @@ -1,15 +1,16 @@ -using Xunit.Abstractions; +// using Xunit.Abstractions; namespace SSW.CleanArchitecture.Architecture.UnitTests.Common; -public static class TypeExtensions -{ - public static void Dump(this IEnumerable types, ITestOutputHelper outputHelper) - { - if (!types.Any()) - outputHelper.WriteLine("No types found."); - - foreach (var type in types) - outputHelper.WriteLine(type.FullName); - } -} \ No newline at end of file +// TODO: Fix up +// public static class TypeExtensions +// { +// public static void Dump(this IEnumerable types, ITestOutputHelper outputHelper) +// { +// if (!types.Any()) +// outputHelper.WriteLine("No types found."); +// +// foreach (var type in types) +// outputHelper.WriteLine(type.FullName); +// } +// } \ No newline at end of file diff --git a/tests/Architecture.Tests/DomainTests.cs b/tests/Architecture.Tests/DomainTests.cs index 0423ab0a..c080aeee 100644 --- a/tests/Architecture.Tests/DomainTests.cs +++ b/tests/Architecture.Tests/DomainTests.cs @@ -12,7 +12,7 @@ public class DomainModel : TestBase private static readonly Type DomainEvent = typeof(IDomainEvent); private static readonly Type ValueObject = typeof(IValueObject); - [Fact] + [Test] public void DomainModel_ShouldInheritsBaseClasses() { // Arrange @@ -37,7 +37,7 @@ public void DomainModel_ShouldInheritsBaseClasses() result.Should().BeSuccessful(); } - [Fact] + [Test] public void EntitiesAndAggregates_ShouldHavePrivateParameterlessConstructor() { var entityTypes = Types.InAssembly(DomainAssembly) diff --git a/tests/Architecture.Tests/Layer.cs b/tests/Architecture.Tests/Layer.cs index d68d9ac0..94e553ab 100644 --- a/tests/Architecture.Tests/Layer.cs +++ b/tests/Architecture.Tests/Layer.cs @@ -4,7 +4,7 @@ namespace SSW.CleanArchitecture.Architecture.UnitTests; public class Layer : TestBase { - [Fact] + [Test] public void DomainLayer_Should_NotHaveDependencyOnApplication() { var result = Types.InAssembly(DomainAssembly) @@ -15,7 +15,7 @@ public void DomainLayer_Should_NotHaveDependencyOnApplication() result.Should().BeSuccessful(); } - [Fact] + [Test] public void DomainLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() { var result = Types.InAssembly(DomainAssembly) @@ -26,7 +26,7 @@ public void DomainLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() result.Should().BeSuccessful(); } - [Fact] + [Test] public void ApplicationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() { var result = Types.InAssembly(ApplicationAssembly) @@ -37,7 +37,7 @@ public void ApplicationLayer_ShouldNotHaveDependencyOn_InfrastructureLayer() result.Should().BeSuccessful(); } - [Fact] + [Test] public void InfrastructureLayer_ShouldNotHaveDependencyOn_PresentationLayer() { var result = Types.InAssembly(InfrastructureAssembly) diff --git a/tests/Architecture.Tests/Presentation.cs b/tests/Architecture.Tests/Presentation.cs index b5a746e5..a5ccce65 100644 --- a/tests/Architecture.Tests/Presentation.cs +++ b/tests/Architecture.Tests/Presentation.cs @@ -9,7 +9,7 @@ public class Presentation : TestBase private static readonly Type IDbContext = typeof(IApplicationDbContext); private static readonly Type DbContext = typeof(ApplicationDbContext); - [Fact] + [Test] public void Endpoints_ShouldNotReferenceDbContext() { var types = Types diff --git a/tests/Architecture.Tests/Usings.cs b/tests/Architecture.Tests/Usings.cs index 8c927eb7..3c84e45e 100644 --- a/tests/Architecture.Tests/Usings.cs +++ b/tests/Architecture.Tests/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using TUnit; \ No newline at end of file From 973cc6e9ec5e0115dd17e9ba9c9ed07f1248b2a3 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 18 Nov 2024 07:01:16 +1000 Subject: [PATCH 03/10] Fixed up test logging --- .../Common/TestResultExtensions.cs | 25 ++++++++++--------- .../Common/TypeExtensions.cs | 25 ++++++++++--------- tests/Domain.UnitTests/Heroes/HeroTests.cs | 10 ++++++++ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/tests/Architecture.Tests/Common/TestResultExtensions.cs b/tests/Architecture.Tests/Common/TestResultExtensions.cs index 3432826c..2cfd8c9c 100644 --- a/tests/Architecture.Tests/Common/TestResultExtensions.cs +++ b/tests/Architecture.Tests/Common/TestResultExtensions.cs @@ -1,20 +1,21 @@ // using Xunit.Abstractions; + +using TUnit.Core.Logging; using TestResult = NetArchTest.Rules.TestResult; namespace SSW.CleanArchitecture.Architecture.UnitTests.Common; public static class TestResultExtensions { -// TODO: Fix up -// public static void DumpFailingTypes(this TestResult result, ITestOutputHelper outputHelper) -// { -// if (result.IsSuccessful) -// return; -// -// outputHelper.WriteLine("Failing Types:"); -// -// foreach (var type in result.FailingTypes) -// outputHelper.WriteLine(type.FullName); -// } -// + public static void DumpFailingTypes(this TestResult result, ILogger logger) + { + if (result.IsSuccessful) + return; + + logger.LogInformation("Failing Types:"); + + foreach (var type in result.FailingTypes) + logger.LogInformation(type.FullName); + } + public static TestResultAssertions Should(this TestResult result) => new(result); } \ No newline at end of file diff --git a/tests/Architecture.Tests/Common/TypeExtensions.cs b/tests/Architecture.Tests/Common/TypeExtensions.cs index 5aa1da6e..d6c111e2 100644 --- a/tests/Architecture.Tests/Common/TypeExtensions.cs +++ b/tests/Architecture.Tests/Common/TypeExtensions.cs @@ -1,16 +1,17 @@ // using Xunit.Abstractions; +using TUnit.Core.Logging; + namespace SSW.CleanArchitecture.Architecture.UnitTests.Common; -// TODO: Fix up -// public static class TypeExtensions -// { -// public static void Dump(this IEnumerable types, ITestOutputHelper outputHelper) -// { -// if (!types.Any()) -// outputHelper.WriteLine("No types found."); -// -// foreach (var type in types) -// outputHelper.WriteLine(type.FullName); -// } -// } \ No newline at end of file +public static class TypeExtensions +{ + public static void Dump(this IEnumerable types, ILogger outputHelper) + { + if (!types.Any()) + outputHelper.LogInformation("No types found."); + + foreach (var type in types) + outputHelper.LogInformation(type.FullName); + } +} \ No newline at end of file diff --git a/tests/Domain.UnitTests/Heroes/HeroTests.cs b/tests/Domain.UnitTests/Heroes/HeroTests.cs index 29634f88..5d226f78 100644 --- a/tests/Domain.UnitTests/Heroes/HeroTests.cs +++ b/tests/Domain.UnitTests/Heroes/HeroTests.cs @@ -1,9 +1,17 @@ using SSW.CleanArchitecture.Domain.Heroes; +using TUnit.Core.Logging; namespace SSW.CleanArchitecture.Domain.UnitTests.Heroes; public class HeroTests { + private readonly DefaultLogger _logger = new(); + + // public HeroTests() + // { + // _logger = logger; + // } + [Test] [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] @@ -27,6 +35,8 @@ public void HeroId_ShouldBeComparable(string stringGuid1, string stringGuid2, bo [Test] public void Create_WithValidNameAndAlias_ShouldSucceed() { + _logger.LogError("Create_WithValidNameAndAlias_ShouldSucceed"); + // Arrange var name = "name"; var alias = "alias"; From e3f3637ef8b807194f334d41e722c69891d17118 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Mon, 18 Nov 2024 07:59:26 +1000 Subject: [PATCH 04/10] Got integration tests working --- .../Common/Fixtures/IntegrationTestBase.cs | 24 +++--- .../IntegrationTestWebApplicationFactory.cs | 44 ---------- .../Common/Fixtures/TestingDatabaseFixture.cs | 57 ------------- .../Common/Fixtures/WebApplicationFactory.cs | 80 +++++++++++++++++++ .../Heroes/Commands/CreateHeroCommandTests.cs | 7 +- .../Heroes/Commands/UpdateHeroCommandTests.cs | 9 ++- .../Heroes/Queries/GetAllHeroesQueryTests.cs | 2 +- .../Commands/AddHeroToTeamCommandTests.cs | 2 +- .../Commands/CompleteMissionCommandTests.cs | 2 +- .../Teams/Commands/CreateTeamCommandTests.cs | 2 +- .../Commands/ExecuteMissionCommandTests.cs | 2 +- .../Events/UpdatePowerLevelEventTests.cs | 2 +- .../Teams/Queries/GetAllTeamsQueryTests.cs | 2 +- tests/WebApi.IntegrationTests/Usings.cs | 4 +- .../WebApi.IntegrationTests.csproj | 29 +++++-- 15 files changed, 132 insertions(+), 136 deletions(-) delete mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestWebApplicationFactory.cs delete mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/TestingDatabaseFixture.cs create mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs index 306915cd..1f2a2801 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs @@ -3,41 +3,41 @@ using Microsoft.Extensions.DependencyInjection; using SSW.CleanArchitecture.Application.Common.Interfaces; using SSW.CleanArchitecture.Infrastructure.Persistence; +using TUnit.Core.Interfaces; namespace WebApi.IntegrationTests.Common.Fixtures; /// /// Integration tests inherit from this to access helper classes /// -[Collection(TestingDatabaseFixtureCollection.Name)] -public abstract class IntegrationTestBase : IAsyncLifetime +// [Collection(TestingDatabaseFixtureCollection.Name)] +public abstract class IntegrationTestBase : IAsyncInitializer { - private readonly IServiceScope _scope; + private IServiceScope _scope; - private readonly TestingDatabaseFixture _fixture; - protected IMediator Mediator { get; } + private WebApplicationFactory _fixture; + protected IMediator Mediator { get; private set; } // TODO: Consider removing this as query results can be cached and cause bad test results // Also, consider encapsulating this and only exposing a `Query` method that internally uses `AsNoTracking()` // see: https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/324 public IApplicationDbContext Context => _dbContext; - private readonly ApplicationDbContext _dbContext; + private ApplicationDbContext _dbContext; protected IQueryable GetQueryable() where T : class => _dbContext.Set().AsNoTracking(); - protected IntegrationTestBase(TestingDatabaseFixture fixture, ITestOutputHelper output) + protected IntegrationTestBase(WebApplicationFactory fixture) { _fixture = fixture; - _fixture.Factory.Output = output; + } + public async Task InitializeAsync() + { _scope = _fixture.ScopeFactory.CreateScope(); Mediator = _scope.ServiceProvider.GetRequiredService(); _dbContext = _scope.ServiceProvider.GetRequiredService(); - } - public async Task InitializeAsync() - { await _fixture.ResetState(); } @@ -46,7 +46,7 @@ protected async Task SaveChangesAsync(CancellationToken cancellationToken = defa await Context.SaveChangesAsync(cancellationToken); } - protected HttpClient GetAnonymousClient() => _fixture.Factory.AnonymousClient.Value; + protected HttpClient GetAnonymousClient() => _fixture.AnonymousClient.Value; public Task DisposeAsync() { diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestWebApplicationFactory.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestWebApplicationFactory.cs deleted file mode 100644 index bf1a6cae..00000000 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestWebApplicationFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Meziantou.Extensions.Logging.Xunit; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MigrationService.Initializers; -using SSW.CleanArchitecture.WebApi; - -namespace WebApi.IntegrationTests.Common.Fixtures; - -/// -/// Host builder (services, DI and configuration) for integration tests -/// -public class WebApiTestFactory : WebApplicationFactory -{ - public DatabaseContainer Database { get; } = new(); - - public ITestOutputHelper Output { get; set; } = null!; - - // NOTE: If you need an authenticated client, create a similar method that performance the authentication, - // adds the appropriate headers, and returns the authenticated client - // For an example of this see https://github.com/SSWConsulting/Northwind365 - public Lazy AnonymousClient => new(CreateClient()); - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - // Redirect application logging to test output - builder.ConfigureLogging(x => - { - x.ClearProviders(); - x.AddFilter(level => level >= LogLevel.Information); - x.Services.AddSingleton(new XUnitLoggerProvider(Output)); - }); - - // Override default DB registration to use out Test Container instead - builder.ConfigureTestServices(services => - { - services.AddScoped(); - }); - - builder.UseSetting("ConnectionStrings:clean-architecture", Database.ConnectionString); - } -} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/TestingDatabaseFixture.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/TestingDatabaseFixture.cs deleted file mode 100644 index 123a62ac..00000000 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/TestingDatabaseFixture.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using MigrationService.Initializers; -using Respawn; - -namespace WebApi.IntegrationTests.Common.Fixtures; - -/// -/// Initializes and resets the database before and after each test -/// -// ReSharper disable once ClassNeverInstantiated.Global -public class TestingDatabaseFixture : IAsyncLifetime -{ - private string ConnectionString => Factory.Database.ConnectionString!; - - private Respawner _checkpoint = default!; - - public IServiceScopeFactory ScopeFactory { get; private set; } = default!; - - public readonly WebApiTestFactory Factory = new(); - - public async Task InitializeAsync() - { - // Initialize DB Container - await Factory.Database.InitializeAsync(); - ScopeFactory = Factory.Services.GetRequiredService(); - - // Create and seed database - using var scope = ScopeFactory.CreateScope(); - var warehouseInitializer = scope.ServiceProvider.GetRequiredService(); - await warehouseInitializer.EnsureDatabaseAsync(default); - await warehouseInitializer.CreateSchemaAsync(true, default); - // await warehouseInitializer.SeedDataAsync(default); - - // NOTE: If there are any tables you want to skip being reset, they can be configured here - _checkpoint = await Respawner.CreateAsync(ConnectionString); - } - - public async Task DisposeAsync() - { - await Factory.Database.DisposeAsync(); - } - - public async Task ResetState() - { - await _checkpoint.ResetAsync(ConnectionString); - } -} - -[CollectionDefinition(Name)] -public class TestingDatabaseFixtureCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. - - public const string Name = nameof(TestingDatabaseFixtureCollection); -} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs new file mode 100644 index 00000000..0100f480 --- /dev/null +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MigrationService.Initializers; +using Respawn; +using SSW.CleanArchitecture.WebApi; +using TUnit.Core.Interfaces; + +namespace WebApi.IntegrationTests.Common.Fixtures; + +/// +/// Initializes and resets the database before and after each test +/// +// ReSharper disable once ClassNeverInstantiated.Global +public class WebApplicationFactory: WebApplicationFactory, IAsyncInitializer +{ + private string ConnectionString => Database.ConnectionString!; + + private Respawner _checkpoint = default!; + + public IServiceScopeFactory ScopeFactory { get; private set; } = default!; + + public async Task InitializeAsync() + { + // Initialize DB Container + await Database.InitializeAsync(); + ScopeFactory = Services.GetRequiredService(); + + // Create and seed database + using var scope = ScopeFactory.CreateScope(); + var warehouseInitializer = scope.ServiceProvider.GetRequiredService(); + await warehouseInitializer.EnsureDatabaseAsync(default); + await warehouseInitializer.CreateSchemaAsync(true, default); + + // NOTE: If there are any tables you want to skip being reset, they can be configured here + _checkpoint = await Respawner.CreateAsync(ConnectionString); + } + + public override async ValueTask DisposeAsync() + { + await Database.DisposeAsync(); + await base.DisposeAsync(); + } + + public async Task ResetState() + { + await _checkpoint.ResetAsync(ConnectionString); + } + + public DatabaseContainer Database { get; } = new(); + + // public ITestOutputHelper Output { get; set; } = null!; + + // NOTE: If you need an authenticated client, create a similar method that performance the authentication, + // adds the appropriate headers, and returns the authenticated client + // For an example of this see https://github.com/SSWConsulting/Northwind365 + public Lazy AnonymousClient => new(CreateClient()); + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Redirect application logging to test output + builder.ConfigureLogging(x => + { + x.ClearProviders(); + x.AddFilter(level => level >= LogLevel.Information); + // TODO: Fix up logging + // x.Services.AddSingleton(new XUnitLoggerProvider(Output)); + }); + + // Override default DB registration to use out Test Container instead + builder.ConfigureTestServices(services => + { + services.AddScoped(); + }); + + builder.UseSetting("ConnectionStrings:clean-architecture", Database.ConnectionString); + } +} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs index ae65965e..e83debb9 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs @@ -6,10 +6,11 @@ namespace WebApi.IntegrationTests.Endpoints.Heroes.Commands; -public class CreateHeroCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +[ClassDataSource(Shared = SharedType.PerTestSession)] +public class CreateHeroCommandTests(WebApplicationFactory fixture) + : IntegrationTestBase(fixture) { - [Fact] + [Test] public async Task Command_ShouldCreateHero() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs index 4276552c..36fc0b37 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs @@ -8,10 +8,11 @@ namespace WebApi.IntegrationTests.Endpoints.Heroes.Commands; -public class UpdateHeroCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +[ClassDataSource(Shared = SharedType.PerTestSession)] +public class UpdateHeroCommandTests(WebApplicationFactory fixture) + : IntegrationTestBase(fixture) { - [Fact] + [Test] public async Task Command_ShouldUpdateHero() { // Arrange @@ -50,7 +51,7 @@ public async Task Command_ShouldUpdateHero() item.UpdatedAt.Should().BeCloseTo(createdTimeStamp, TimeSpan.FromSeconds(10)); } - [Fact] + [Test] public async Task Command_WhenHeroDoesNotExist_ShouldReturnNotFound() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs index 0e49c2ff..8a4e5c3e 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs @@ -8,7 +8,7 @@ namespace WebApi.IntegrationTests.Endpoints.Heroes.Queries; public class GetAllHeroesQueryTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Query_ShouldReturnAllHeroes() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs index 06b39bab..18ec57e2 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs @@ -10,7 +10,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; public class AddHeroToTeamCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Command_ShouldAddHeroToTeam() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs index 33e13e62..bc81bf6e 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs @@ -10,7 +10,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; public class CompleteMissionCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Command_ShouldCompleteMission() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs index 96b604f6..112173f2 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs @@ -9,7 +9,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; public class CreateTeamCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Command_ShouldCreateTeam() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs index a84e3fd0..dc4577d2 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs @@ -12,7 +12,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; public class ExecuteMissionCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Command_ShouldExecuteMission() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs index 0909b8a0..74c99fb5 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs @@ -13,7 +13,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Events; public class UpdatePowerLevelEventTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Command_UpdatePowerOnTeam() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs index 881449af..2f43a4ae 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs @@ -8,7 +8,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Queries; public class GetAllTeamsQueryTests(TestingDatabaseFixture fixture, ITestOutputHelper output) : IntegrationTestBase(fixture, output) { - [Fact] + [Test] public async Task Query_ShouldReturnAllTeams() { // Arrange diff --git a/tests/WebApi.IntegrationTests/Usings.cs b/tests/WebApi.IntegrationTests/Usings.cs index 339afbbd..a1bcbf32 100644 --- a/tests/WebApi.IntegrationTests/Usings.cs +++ b/tests/WebApi.IntegrationTests/Usings.cs @@ -1,3 +1,3 @@ -global using Xunit; -global using Xunit.Abstractions; +// global using Xunit; +// global using Xunit.Abstractions; global using FluentAssertions; \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj index c31cdb07..61e967d7 100644 --- a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj +++ b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj @@ -14,19 +14,19 @@ - + - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + @@ -34,4 +34,19 @@ + + + + + + + + + + + + + + + From 84c0cd9daa5f1e280c9bf9871f355dbfb64fe03e Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:27:28 +1000 Subject: [PATCH 05/10] Massive refactor of integration tests --- .../Fixtures/CustomWebApplicationFactory.cs | 34 ++++++++ .../Common/Fixtures/DatabaseContainer.cs | 6 +- .../Common/Fixtures/IntegrationTestBase.cs | 50 +++++------- .../Common/Fixtures/SqlServerTestDatabase.cs | 42 ++++++++++ .../Common/Fixtures/Testing.cs | 44 ++++++++++ .../Common/Fixtures/WebApplicationFactory.cs | 80 ------------------- .../Heroes/Commands/CreateHeroCommandTests.cs | 7 +- .../Heroes/Commands/UpdateHeroCommandTests.cs | 11 +-- .../Heroes/Queries/GetAllHeroesQueryTests.cs | 6 +- .../Commands/AddHeroToTeamCommandTests.cs | 8 +- .../Commands/CompleteMissionCommandTests.cs | 7 +- .../Teams/Commands/CreateTeamCommandTests.cs | 6 +- .../Commands/ExecuteMissionCommandTests.cs | 7 +- .../Events/UpdatePowerLevelEventTests.cs | 7 +- .../Teams/Queries/GetAllTeamsQueryTests.cs | 7 +- .../WebApi.IntegrationTests.csproj | 21 ----- 16 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs create mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs create mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs delete mode 100644 tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs new file mode 100644 index 00000000..dfd477ad --- /dev/null +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Logging; +using SSW.CleanArchitecture.WebApi; +using System.Data.Common; + +namespace WebApi.IntegrationTests.Common.Fixtures; + +/// +/// Leverages default host setup to allow for integration testing +/// +public class CustomWebApplicationFactory: WebApplicationFactory +{ + private readonly DbConnection _dbConnection; + + public CustomWebApplicationFactory(DbConnection dbConnection) + { + _dbConnection = dbConnection; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Redirect application logging to test output + builder.ConfigureLogging(x => + { + x.ClearProviders(); + x.AddFilter(level => level >= LogLevel.Information); + // TODO: Fix up logging + // x.Services.AddSingleton(new XUnitLoggerProvider(Output)); + }); + + builder.UseSetting("ConnectionStrings:clean-architecture", _dbConnection.ConnectionString); + } +} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs index 2db2d3c4..7a402ec3 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs @@ -1,3 +1,5 @@ +using Microsoft.Data.SqlClient; +using MigrationService.Initializers; using Polly; using Testcontainers.MsSql; @@ -18,12 +20,12 @@ public class DatabaseContainer : IAsyncDisposable private const int MaxRetries = 5; - public string? ConnectionString { get; private set; } + public SqlConnection? ConnectionString { get; private set; } public async Task InitializeAsync() { await StartWithRetry(); - ConnectionString = _container.GetConnectionString(); + ConnectionString = new SqlConnection(_container.GetConnectionString()); } private async Task StartWithRetry() diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs index 1f2a2801..2aedbd57 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs @@ -1,6 +1,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Instrumentation.Http; using SSW.CleanArchitecture.Application.Common.Interfaces; using SSW.CleanArchitecture.Infrastructure.Persistence; using TUnit.Core.Interfaces; @@ -10,47 +11,40 @@ namespace WebApi.IntegrationTests.Common.Fixtures; /// /// Integration tests inherit from this to access helper classes /// -// [Collection(TestingDatabaseFixtureCollection.Name)] -public abstract class IntegrationTestBase : IAsyncInitializer +[NotInParallel] +public abstract class IntegrationTestBaseV2 : IDisposable { - private IServiceScope _scope; + private readonly ApplicationDbContext _dbContext; + private readonly IServiceScope _scope; + protected readonly ISender Mediator; - private WebApplicationFactory _fixture; - protected IMediator Mediator { get; private set; } - - // TODO: Consider removing this as query results can be cached and cause bad test results - // Also, consider encapsulating this and only exposing a `Query` method that internally uses `AsNoTracking()` - // see: https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/324 - public IApplicationDbContext Context => _dbContext; - - private ApplicationDbContext _dbContext; - - protected IQueryable GetQueryable() where T : class => _dbContext.Set().AsNoTracking(); - - protected IntegrationTestBase(WebApplicationFactory fixture) + public IntegrationTestBaseV2() { - _fixture = fixture; + _scope = Testing.CreateScope(); + _dbContext = _scope.ServiceProvider.GetRequiredService(); + Mediator = _scope.ServiceProvider.GetRequiredService(); } - public async Task InitializeAsync() - { - _scope = _fixture.ScopeFactory.CreateScope(); - Mediator = _scope.ServiceProvider.GetRequiredService(); - _dbContext = _scope.ServiceProvider.GetRequiredService(); + protected IQueryable GetQueryable() where T : class => _dbContext.Set().AsNoTracking(); - await _fixture.ResetState(); + protected async Task AddAsync(TEntity entity) + where TEntity : class + { + await _dbContext.AddAsync(entity); + await _dbContext.SaveChangesAsync(); } - protected async Task SaveChangesAsync(CancellationToken cancellationToken = default) + protected async Task AddRangeAsync(IEnumerable entities) + where TEntity : class { - await Context.SaveChangesAsync(cancellationToken); + await _dbContext.AddRangeAsync(entities); + await _dbContext.SaveChangesAsync(); } - protected HttpClient GetAnonymousClient() => _fixture.AnonymousClient.Value; + protected HttpClient GetAnonymousClient() => Testing.AnonymousClient.Value; - public Task DisposeAsync() + public void Dispose() { _scope.Dispose(); - return Task.CompletedTask; } } \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs new file mode 100644 index 00000000..190d6fc1 --- /dev/null +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Respawn; +using SSW.CleanArchitecture.Infrastructure.Persistence; +using System.Data.Common; + +namespace WebApi.IntegrationTests.Common.Fixtures; + +public class SqlServerTestDatabase : IAsyncDisposable +{ + private static DatabaseContainer _database = new(); + private Respawner _checkpoint = null!; + + /// + /// Create and seed database + /// + public async Task InitializeAsync() + { + await _database.InitializeAsync(); + + var options = new DbContextOptionsBuilder() + .UseSqlServer(_database.ConnectionString) + .Options; + + await using var dbContext = new ApplicationDbContext(options); + await dbContext.Database.MigrateAsync(); + + _checkpoint = await Respawner.CreateAsync(_database.ConnectionString, + new RespawnerOptions { TablesToIgnore = ["__EFMigrationsHistory"] }); + } + + public DbConnection GetConnection() => _database.ConnectionString; + + public async Task ResetAsync() + { + await _checkpoint.ResetAsync(_database.ConnectionString); + } + + public async ValueTask DisposeAsync() + { + await _database.DisposeAsync(); + } +} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs new file mode 100644 index 00000000..8e8fb22b --- /dev/null +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace WebApi.IntegrationTests.Common.Fixtures; + +public static class Testing +{ + private static SqlServerTestDatabase _database = new(); + private static CustomWebApplicationFactory _factory = null!; + private static IServiceScopeFactory _scopeFactory = null!; + + [Before(Assembly)] + public static async Task GlobalSetup() + { + await _database.InitializeAsync(); + _factory = new CustomWebApplicationFactory(_database.GetConnection()); + _scopeFactory = _factory.Services.GetRequiredService(); + } + + [BeforeEvery(Test)] + public static async Task TestSetup(TestContext context) + { + await _database.ResetAsync(); + } + + [AfterEvery(Test)] + public static async Task TestCleanUp(TestContext context) + { + // TODO: Anything to do here? + } + + [After(Assembly)] + public static async Task GlobalCleanUp() + { + await _database.DisposeAsync(); + await _factory.DisposeAsync(); + } + + // NOTE: If you need an authenticated client, create a similar method that performance the authentication, + // adds the appropriate headers, and returns the authenticated client + // For an example of this see https://github.com/SSWConsulting/Northwind365 + public static Lazy AnonymousClient => new(_factory.CreateClient()); + + public static IServiceScope CreateScope() => _scopeFactory.CreateScope(); +} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs deleted file mode 100644 index 0100f480..00000000 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/WebApplicationFactory.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MigrationService.Initializers; -using Respawn; -using SSW.CleanArchitecture.WebApi; -using TUnit.Core.Interfaces; - -namespace WebApi.IntegrationTests.Common.Fixtures; - -/// -/// Initializes and resets the database before and after each test -/// -// ReSharper disable once ClassNeverInstantiated.Global -public class WebApplicationFactory: WebApplicationFactory, IAsyncInitializer -{ - private string ConnectionString => Database.ConnectionString!; - - private Respawner _checkpoint = default!; - - public IServiceScopeFactory ScopeFactory { get; private set; } = default!; - - public async Task InitializeAsync() - { - // Initialize DB Container - await Database.InitializeAsync(); - ScopeFactory = Services.GetRequiredService(); - - // Create and seed database - using var scope = ScopeFactory.CreateScope(); - var warehouseInitializer = scope.ServiceProvider.GetRequiredService(); - await warehouseInitializer.EnsureDatabaseAsync(default); - await warehouseInitializer.CreateSchemaAsync(true, default); - - // NOTE: If there are any tables you want to skip being reset, they can be configured here - _checkpoint = await Respawner.CreateAsync(ConnectionString); - } - - public override async ValueTask DisposeAsync() - { - await Database.DisposeAsync(); - await base.DisposeAsync(); - } - - public async Task ResetState() - { - await _checkpoint.ResetAsync(ConnectionString); - } - - public DatabaseContainer Database { get; } = new(); - - // public ITestOutputHelper Output { get; set; } = null!; - - // NOTE: If you need an authenticated client, create a similar method that performance the authentication, - // adds the appropriate headers, and returns the authenticated client - // For an example of this see https://github.com/SSWConsulting/Northwind365 - public Lazy AnonymousClient => new(CreateClient()); - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - // Redirect application logging to test output - builder.ConfigureLogging(x => - { - x.ClearProviders(); - x.AddFilter(level => level >= LogLevel.Information); - // TODO: Fix up logging - // x.Services.AddSingleton(new XUnitLoggerProvider(Output)); - }); - - // Override default DB registration to use out Test Container instead - builder.ConfigureTestServices(services => - { - services.AddScoped(); - }); - - builder.UseSetting("ConnectionStrings:clean-architecture", Database.ConnectionString); - } -} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs index e83debb9..9d41e0b2 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/CreateHeroCommandTests.cs @@ -1,14 +1,13 @@ using Microsoft.EntityFrameworkCore; using SSW.CleanArchitecture.Application.UseCases.Heroes.Commands.CreateHero; +using SSW.CleanArchitecture.Domain.Heroes; using System.Net; using System.Net.Http.Json; using WebApi.IntegrationTests.Common.Fixtures; namespace WebApi.IntegrationTests.Endpoints.Heroes.Commands; -[ClassDataSource(Shared = SharedType.PerTestSession)] -public class CreateHeroCommandTests(WebApplicationFactory fixture) - : IntegrationTestBase(fixture) +public class CreateHeroCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldCreateHero() @@ -31,7 +30,7 @@ public async Task Command_ShouldCreateHero() // Assert result.StatusCode.Should().Be(HttpStatusCode.Created); - var item = await Context.Heroes.AsNoTracking().FirstAsync(); + var item = await GetQueryable().FirstAsync(); item.Should().NotBeNull(); item.Name.Should().Be(cmd.Name); diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs index 36fc0b37..7ca89042 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Commands/UpdateHeroCommandTests.cs @@ -8,9 +8,7 @@ namespace WebApi.IntegrationTests.Endpoints.Heroes.Commands; -[ClassDataSource(Shared = SharedType.PerTestSession)] -public class UpdateHeroCommandTests(WebApplicationFactory fixture) - : IntegrationTestBase(fixture) +public class UpdateHeroCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldUpdateHero() @@ -19,8 +17,7 @@ public async Task Command_ShouldUpdateHero() var heroName = "2021-01-01T00:00:00Z"; var heroAlias = "2021-01-01T00:00:00Z-alias"; var hero = HeroFactory.Generate(); - Context.Heroes.Add(hero); - await Context.SaveChangesAsync(); + await AddAsync(hero); (string Name, int PowerLevel)[] powers = [ ("Heat vision", 7), @@ -40,7 +37,7 @@ public async Task Command_ShouldUpdateHero() // Assert result.StatusCode.Should().Be(HttpStatusCode.NoContent); - Hero item = await Context.Heroes.AsNoTracking().FirstAsync(dbHero => dbHero.Id == hero.Id); + var item = await GetQueryable().FirstAsync(dbHero => dbHero.Id == hero.Id); item.Should().NotBeNull(); item.Name.Should().Be(cmd.Name); @@ -68,7 +65,7 @@ public async Task Command_WhenHeroDoesNotExist_ShouldReturnNotFound() // Assert result.StatusCode.Should().Be(HttpStatusCode.NotFound); - Hero? item = await Context.Heroes.AsNoTracking().FirstOrDefaultAsync(dbHero => dbHero.Id == heroId); + var item = await GetQueryable().FirstOrDefaultAsync(dbHero => dbHero.Id == heroId); item.Should().BeNull(); } diff --git a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs index 8a4e5c3e..6e046435 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Heroes/Queries/GetAllHeroesQueryTests.cs @@ -5,8 +5,7 @@ namespace WebApi.IntegrationTests.Endpoints.Heroes.Queries; -public class GetAllHeroesQueryTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class GetAllHeroesQueryTests : IntegrationTestBaseV2 { [Test] public async Task Query_ShouldReturnAllHeroes() @@ -14,8 +13,7 @@ public async Task Query_ShouldReturnAllHeroes() // Arrange const int entityCount = 10; var entities = HeroFactory.Generate(entityCount); - await Context.Heroes.AddRangeAsync(entities); - await Context.SaveChangesAsync(); + await AddRangeAsync(entities); var client = GetAnonymousClient(); // Act diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs index 18ec57e2..93cee3c6 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/AddHeroToTeamCommandTests.cs @@ -7,8 +7,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; -public class AddHeroToTeamCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class AddHeroToTeamCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldAddHeroToTeam() @@ -16,9 +15,8 @@ public async Task Command_ShouldAddHeroToTeam() // Arrange var hero = HeroFactory.Generate(); var team = TeamFactory.Generate(); - Context.Heroes.Add(hero); - Context.Teams.Add(team); - await Context.SaveChangesAsync(); + await AddAsync(hero); + await AddAsync(team); var teamId = team.Id.Value; var heroId = hero.Id.Value; diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs index bc81bf6e..661b39dd 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CompleteMissionCommandTests.cs @@ -7,8 +7,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; -public class CompleteMissionCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class CompleteMissionCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldCompleteMission() @@ -18,8 +17,8 @@ public async Task Command_ShouldCompleteMission() var team = TeamFactory.Generate(); team.AddHero(hero); team.ExecuteMission("Save the world"); - Context.Teams.Add(team); - await Context.SaveChangesAsync(); + await AddAsync(team); + // await Context.SaveChangesAsync(); var teamId = team.Id.Value; var client = GetAnonymousClient(); diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs index 112173f2..53c2a785 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/CreateTeamCommandTests.cs @@ -1,13 +1,13 @@ using Microsoft.EntityFrameworkCore; using SSW.CleanArchitecture.Application.UseCases.Teams.Commands.CreateTeam; +using SSW.CleanArchitecture.Domain.Teams; using System.Net; using System.Net.Http.Json; using WebApi.IntegrationTests.Common.Fixtures; namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; -public class CreateTeamCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class CreateTeamCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldCreateTeam() @@ -21,7 +21,7 @@ public async Task Command_ShouldCreateTeam() // Assert result.StatusCode.Should().Be(HttpStatusCode.Created); - var item = await Context.Teams.AsNoTracking().FirstAsync(); + var item = await GetQueryable().FirstAsync(); item.Should().NotBeNull(); item.Name.Should().Be(cmd.Name); diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs index dc4577d2..6bb64be0 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Commands/ExecuteMissionCommandTests.cs @@ -9,8 +9,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Commands; -public class ExecuteMissionCommandTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class ExecuteMissionCommandTests : IntegrationTestBaseV2 { [Test] public async Task Command_ShouldExecuteMission() @@ -19,8 +18,8 @@ public async Task Command_ShouldExecuteMission() var hero = HeroFactory.Generate(); var team = TeamFactory.Generate(); team.AddHero(hero); - Context.Teams.Add(team); - await Context.SaveChangesAsync(); + await AddAsync(team); + // await Context.SaveChangesAsync(); var teamId = team.Id.Value; var client = GetAnonymousClient(); var request = new ExecuteMissionCommand("Save the world"); diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs index 74c99fb5..231e66ea 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs @@ -10,8 +10,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Events; -public class UpdatePowerLevelEventTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class UpdatePowerLevelEventTests : IntegrationTestBaseV2 { [Test] public async Task Command_UpdatePowerOnTeam() @@ -22,8 +21,8 @@ public async Task Command_UpdatePowerOnTeam() List powers = [new Power("Strength", 10)]; hero.UpdatePowers(powers); team.AddHero(hero); - Context.Teams.Add(team); - await Context.SaveChangesAsync(); + await AddAsync(team); + // await Context.SaveChangesAsync(); powers.Add(new Power("Speed", 5)); var powerDtos = powers.Select(p => new UpdateHeroPowerDto { Name = p.Name, PowerLevel = p.PowerLevel }); var cmd = new UpdateHeroCommand(hero.Name, hero.Alias, powerDtos); diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs index 2f43a4ae..ca6d642b 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Queries/GetAllTeamsQueryTests.cs @@ -5,8 +5,7 @@ namespace WebApi.IntegrationTests.Endpoints.Teams.Queries; -public class GetAllTeamsQueryTests(TestingDatabaseFixture fixture, ITestOutputHelper output) - : IntegrationTestBase(fixture, output) +public class GetAllTeamsQueryTests : IntegrationTestBaseV2 { [Test] public async Task Query_ShouldReturnAllTeams() @@ -14,8 +13,8 @@ public async Task Query_ShouldReturnAllTeams() // Arrange const int entityCount = 10; var entities = TeamFactory.Generate(entityCount); - await Context.Teams.AddRangeAsync(entities); - await Context.SaveChangesAsync(); + await AddRangeAsync(entities); + // await Context.SaveChangesAsync(); var client = GetAnonymousClient(); // Act diff --git a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj index 61e967d7..7138230b 100644 --- a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj +++ b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj @@ -14,7 +14,6 @@ - @@ -22,11 +21,6 @@ - - - - - @@ -34,19 +28,4 @@ - - - - - - - - - - - - - - - From ed27031ba7671da2058f6067dd7ad7e0f404eb64 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:35:05 +1000 Subject: [PATCH 06/10] Got integration tests working --- .editorconfig | 2 +- .../Common/Fixtures/DatabaseContainer.cs | 6 +++-- .../Common/Fixtures/IntegrationTestBase.cs | 13 ++++------ .../Common/Fixtures/SqlServerTestDatabase.cs | 24 +++++++++++++++---- .../Common/Fixtures/Testing.cs | 4 ++-- .../WebApi.IntegrationTests.csproj | 4 ++++ 6 files changed, 34 insertions(+), 19 deletions(-) diff --git a/.editorconfig b/.editorconfig index e3ba3496..f65322c8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -242,7 +242,7 @@ dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs index 7a402ec3..331f5542 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs @@ -13,6 +13,8 @@ public class DatabaseContainer : IAsyncDisposable private readonly MsSqlContainer _container = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") .WithName($"CleanArchitecture-IntegrationTests-{Guid.CreateVersion7()}") + // .WithCommand("sqlcmd -Q \"CREATE DATABASE [CleanArchitecture-IntegrationTests]\"") + // .WithCommand("/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Password123 -Q \"CREATE DATABASE [MyDatabase]\"") .WithPassword("Password123") .WithPortBinding(1433, true) .WithAutoRemove(true) @@ -20,12 +22,12 @@ public class DatabaseContainer : IAsyncDisposable private const int MaxRetries = 5; - public SqlConnection? ConnectionString { get; private set; } + public SqlConnection? Connection { get; private set; } public async Task InitializeAsync() { await StartWithRetry(); - ConnectionString = new SqlConnection(_container.GetConnectionString()); + Connection = new SqlConnection(_container.GetConnectionString()); } private async Task StartWithRetry() diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs index 2aedbd57..9bd93b4e 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs @@ -1,10 +1,6 @@ -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Instrumentation.Http; -using SSW.CleanArchitecture.Application.Common.Interfaces; using SSW.CleanArchitecture.Infrastructure.Persistence; -using TUnit.Core.Interfaces; namespace WebApi.IntegrationTests.Common.Fixtures; @@ -14,15 +10,14 @@ namespace WebApi.IntegrationTests.Common.Fixtures; [NotInParallel] public abstract class IntegrationTestBaseV2 : IDisposable { - private readonly ApplicationDbContext _dbContext; - private readonly IServiceScope _scope; - protected readonly ISender Mediator; + private ApplicationDbContext _dbContext = null!; + private IServiceScope _scope = null!; - public IntegrationTestBaseV2() + [Before(Test)] + public void TestSetup() { _scope = Testing.CreateScope(); _dbContext = _scope.ServiceProvider.GetRequiredService(); - Mediator = _scope.ServiceProvider.GetRequiredService(); } protected IQueryable GetQueryable() where T : class => _dbContext.Set().AsNoTracking(); diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs index 190d6fc1..29b96c61 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs @@ -1,3 +1,4 @@ +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Respawn; using SSW.CleanArchitecture.Infrastructure.Persistence; @@ -9,6 +10,7 @@ public class SqlServerTestDatabase : IAsyncDisposable { private static DatabaseContainer _database = new(); private Respawner _checkpoint = null!; + private string _connectionString = null!; /// /// Create and seed database @@ -17,22 +19,34 @@ public async Task InitializeAsync() { await _database.InitializeAsync(); + var builder = new SqlConnectionStringBuilder(_database.Connection.ConnectionString) + { + InitialCatalog = "CleanArchitecture-IntegrationTests" + }; + + _connectionString = builder.ConnectionString; + + // _connection = new SqlConnection(_database.Connection.ConnectionString); + // _connection.da("CleanArchitecture-IntegrationTests"); + + // _connection = _database.Connection; + var options = new DbContextOptionsBuilder() - .UseSqlServer(_database.ConnectionString) + .UseSqlServer(_connectionString) .Options; - await using var dbContext = new ApplicationDbContext(options); + using var dbContext = new ApplicationDbContext(options); await dbContext.Database.MigrateAsync(); - _checkpoint = await Respawner.CreateAsync(_database.ConnectionString, + _checkpoint = await Respawner.CreateAsync(_connectionString, new RespawnerOptions { TablesToIgnore = ["__EFMigrationsHistory"] }); } - public DbConnection GetConnection() => _database.ConnectionString; + public DbConnection GetConnection() => new SqlConnection(_connectionString); public async Task ResetAsync() { - await _checkpoint.ResetAsync(_database.ConnectionString); + await _checkpoint.ResetAsync(_connectionString); } public async ValueTask DisposeAsync() diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs index 8e8fb22b..c06d918b 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs @@ -19,13 +19,13 @@ public static async Task GlobalSetup() [BeforeEvery(Test)] public static async Task TestSetup(TestContext context) { - await _database.ResetAsync(); + // Nothing to do yet } [AfterEvery(Test)] public static async Task TestCleanUp(TestContext context) { - // TODO: Anything to do here? + await _database.ResetAsync(); } [After(Assembly)] diff --git a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj index 7138230b..ee3f1ece 100644 --- a/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj +++ b/tests/WebApi.IntegrationTests/WebApi.IntegrationTests.csproj @@ -28,4 +28,8 @@ + + + + From c6d3c0d48dfb0e0e206ef04189f351815c483602 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:53:40 +1000 Subject: [PATCH 07/10] Add handling for eventual consistency. --- .../Common/Fixtures/DatabaseContainer.cs | 4 +--- .../Common/Fixtures/IntegrationTestBase.cs | 5 +++++ tests/WebApi.IntegrationTests/Common/Wait.cs | 14 ++++++++++++++ .../Teams/Events/UpdatePowerLevelEventTests.cs | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/WebApi.IntegrationTests/Common/Wait.cs diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs index 331f5542..985c84b8 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs @@ -12,9 +12,7 @@ public class DatabaseContainer : IAsyncDisposable { private readonly MsSqlContainer _container = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") - .WithName($"CleanArchitecture-IntegrationTests-{Guid.CreateVersion7()}") - // .WithCommand("sqlcmd -Q \"CREATE DATABASE [CleanArchitecture-IntegrationTests]\"") - // .WithCommand("/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Password123 -Q \"CREATE DATABASE [MyDatabase]\"") + .WithName($"CleanArchitecture-IntegrationTests-{Guid.NewGuid()}") .WithPassword("Password123") .WithPortBinding(1433, true) .WithAutoRemove(true) diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs index 9bd93b4e..fb811779 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/IntegrationTestBase.cs @@ -36,6 +36,11 @@ protected async Task AddRangeAsync(IEnumerable entities) await _dbContext.SaveChangesAsync(); } + protected async Task SaveAsync() + { + await _dbContext.SaveChangesAsync(); + } + protected HttpClient GetAnonymousClient() => Testing.AnonymousClient.Value; public void Dispose() diff --git a/tests/WebApi.IntegrationTests/Common/Wait.cs b/tests/WebApi.IntegrationTests/Common/Wait.cs new file mode 100644 index 00000000..8e7e9657 --- /dev/null +++ b/tests/WebApi.IntegrationTests/Common/Wait.cs @@ -0,0 +1,14 @@ +namespace WebApi.IntegrationTests.Common; + +internal static class Wait +{ + private const int Milliseconds = 1000; + + /// + /// Add a delay to allow the event to be processed + /// + internal static async Task ForEventualConsistency() + { + await Task.Delay(Milliseconds); + } +} \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs index 231e66ea..3201a024 100644 --- a/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs +++ b/tests/WebApi.IntegrationTests/Endpoints/Teams/Events/UpdatePowerLevelEventTests.cs @@ -5,6 +5,7 @@ using SSW.CleanArchitecture.Domain.Teams; using System.Net; using System.Net.Http.Json; +using WebApi.IntegrationTests.Common; using WebApi.IntegrationTests.Common.Factories; using WebApi.IntegrationTests.Common.Fixtures; @@ -33,6 +34,7 @@ public async Task Command_UpdatePowerOnTeam() var result = await client.PutAsJsonAsync($"/api/heroes/{cmd.HeroId}", cmd); // Assert + await Wait.ForEventualConsistency(); var updatedTeam = await GetQueryable() .WithSpecification(new TeamByIdSpec(team.Id)) .FirstOrDefaultAsync(); From 58c5bd2d17e1ad908b9ccfb9518b7f60b9becd18 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:55:00 +1000 Subject: [PATCH 08/10] Tidy up code --- .../Common/Fixtures/DatabaseContainer.cs | 1 - .../Common/Fixtures/SqlServerTestDatabase.cs | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs index 985c84b8..b8c7a329 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/DatabaseContainer.cs @@ -1,5 +1,4 @@ using Microsoft.Data.SqlClient; -using MigrationService.Initializers; using Polly; using Testcontainers.MsSql; diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs index 29b96c61..eceeddfd 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/SqlServerTestDatabase.cs @@ -8,7 +8,7 @@ namespace WebApi.IntegrationTests.Common.Fixtures; public class SqlServerTestDatabase : IAsyncDisposable { - private static DatabaseContainer _database = new(); + private readonly DatabaseContainer _database = new(); private Respawner _checkpoint = null!; private string _connectionString = null!; @@ -26,11 +26,6 @@ public async Task InitializeAsync() _connectionString = builder.ConnectionString; - // _connection = new SqlConnection(_database.Connection.ConnectionString); - // _connection.da("CleanArchitecture-IntegrationTests"); - - // _connection = _database.Connection; - var options = new DbContextOptionsBuilder() .UseSqlServer(_connectionString) .Options; From e354f406ca40343b91c34799e7914d24095d1099 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 23 Nov 2024 06:46:39 +1000 Subject: [PATCH 09/10] Remove unneeded constructor --- tests/Domain.UnitTests/Heroes/HeroTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Domain.UnitTests/Heroes/HeroTests.cs b/tests/Domain.UnitTests/Heroes/HeroTests.cs index 5d226f78..192862d1 100644 --- a/tests/Domain.UnitTests/Heroes/HeroTests.cs +++ b/tests/Domain.UnitTests/Heroes/HeroTests.cs @@ -7,11 +7,6 @@ public class HeroTests { private readonly DefaultLogger _logger = new(); - // public HeroTests() - // { - // _logger = logger; - // } - [Test] [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "cc3431a8-4a31-4f76-af64-e8198279d7a4", false)] [Arguments("c8ad9974-ca93-44a5-9215-2f4d9e866c7a", "c8ad9974-ca93-44a5-9215-2f4d9e866c7a", true)] From 52265cfea2adb7f24faae06de0841252b10384a6 Mon Sep 17 00:00:00 2001 From: "Daniel Mackay [SSW]" <2636640+danielmackay@users.noreply.github.com> Date: Sat, 23 Nov 2024 10:07:34 +1000 Subject: [PATCH 10/10] Add TUnit logger --- .../Commands/CreateHero/CreateHeroCommand.cs | 7 ++- .../Fixtures/CustomWebApplicationFactory.cs | 46 +++++++++++++++++-- .../Common/Fixtures/Testing.cs | 2 +- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/Application/UseCases/Heroes/Commands/CreateHero/CreateHeroCommand.cs b/src/Application/UseCases/Heroes/Commands/CreateHero/CreateHeroCommand.cs index 0b8bb357..112b16a2 100644 --- a/src/Application/UseCases/Heroes/Commands/CreateHero/CreateHeroCommand.cs +++ b/src/Application/UseCases/Heroes/Commands/CreateHero/CreateHeroCommand.cs @@ -1,4 +1,5 @@ -using SSW.CleanArchitecture.Application.Common.Interfaces; +using Microsoft.Extensions.Logging; +using SSW.CleanArchitecture.Application.Common.Interfaces; using SSW.CleanArchitecture.Domain.Heroes; namespace SSW.CleanArchitecture.Application.UseCases.Heroes.Commands.CreateHero; @@ -9,11 +10,13 @@ public sealed record CreateHeroCommand( IEnumerable Powers) : IRequest>; // ReSharper disable once UnusedType.Global -public sealed class CreateHeroCommandHandler(IApplicationDbContext dbContext) +public sealed class CreateHeroCommandHandler(IApplicationDbContext dbContext, ILogger logger) : IRequestHandler> { public async Task> Handle(CreateHeroCommand request, CancellationToken cancellationToken) { + logger.LogError("Creating hero with name {Name} and alias {Alias}", request.Name, request.Alias); + var hero = Hero.Create(request.Name, request.Alias); var powers = request.Powers.Select(p => new Power(p.Name, p.PowerLevel)); hero.UpdatePowers(powers); diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs index dfd477ad..dcd363e6 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/CustomWebApplicationFactory.cs @@ -1,8 +1,12 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SSW.CleanArchitecture.WebApi; using System.Data.Common; +using TUnit.Core.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace WebApi.IntegrationTests.Common.Fixtures; @@ -21,14 +25,46 @@ public CustomWebApplicationFactory(DbConnection dbConnection) protected override void ConfigureWebHost(IWebHostBuilder builder) { // Redirect application logging to test output - builder.ConfigureLogging(x => + builder.ConfigureLogging(logging => { - x.ClearProviders(); - x.AddFilter(level => level >= LogLevel.Information); - // TODO: Fix up logging - // x.Services.AddSingleton(new XUnitLoggerProvider(Output)); + logging.ClearProviders(); + // TODO: This doesn't seem to log everything. Need to investigate further + logging.AddProvider(new TUnitLoggerProvider()); }); builder.UseSetting("ConnectionStrings:clean-architecture", _dbConnection.ConnectionString); } +} + +public class TUnitLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) + { + return new TUnitDefaultLogger(); + } + + public void Dispose() + { + // Dispose resources if any + } +} + +public class TUnitDefaultLogger : ILogger +{ + private readonly TUnit.Core.Logging.DefaultLogger _logger = new(); + + public IDisposable BeginScope(TState state) + { + return null!; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _logger.LogInformation(formatter(state, exception)); + } } \ No newline at end of file diff --git a/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs index c06d918b..e7059c55 100644 --- a/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs +++ b/tests/WebApi.IntegrationTests/Common/Fixtures/Testing.cs @@ -4,7 +4,7 @@ namespace WebApi.IntegrationTests.Common.Fixtures; public static class Testing { - private static SqlServerTestDatabase _database = new(); + private static readonly SqlServerTestDatabase _database = new(); private static CustomWebApplicationFactory _factory = null!; private static IServiceScopeFactory _scopeFactory = null!;