From d291e5551c7fc81d328e509c3657cd5a493a5caa Mon Sep 17 00:00:00 2001 From: Aman Toumaj Date: Thu, 22 Jan 2026 19:34:42 +0100 Subject: [PATCH] Adding architectural tests and refactoring code according new architectural tests --- DomainDrivenTutorial.sln | 7 ++ .../ConventionTests.cs | 36 ++++++++++ .../DomainRulesTests.cs | 72 +++++++++++++++++++ .../EShoppingTutorial.ArchTests.csproj | 30 ++++++++ .../GlobalUsings.cs | 2 + .../EShoppingTutorial.ArchTests/LayerTests.cs | 37 ++++++++++ .../RepositoryTests.cs | 49 +++++++++++++ .../CreateOrder/CreateOrderHandler.cs | 2 +- .../DeleteOrder/DeleteOrderHandler.cs | 2 +- .../GetAllOrders/GetAllOrdersHandler.cs | 2 +- .../GetOrderById/GetOrderByIdHandler.cs | 2 +- .../GetPagedOrders/GetPagedOrdersHandler.cs | 2 +- .../EShoppingTutorial.Core.Persistence.csproj | 4 ++ .../EShoppingTutorial.Core/Entities/Order.cs | 2 +- .../ValueObjects/Address.cs | 2 +- .../ValueObjects/Price.cs | 2 +- .../ValueObjects/StronglyTypedIDs.cs | 8 +-- .../Domain/Entities/OrderUnitTests.cs | 17 +++-- .../EShoppingTutorial.UnitTests.csproj | 1 + .../GlobalUsings.cs | 1 + .../OrderRepositoryIntegrationTests.cs | 3 +- 21 files changed, 264 insertions(+), 19 deletions(-) create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/ConventionTests.cs create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/DomainRulesTests.cs create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/EShoppingTutorial.ArchTests.csproj create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/GlobalUsings.cs create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/LayerTests.cs create mode 100644 EShoppingTutorial/EShoppingTutorial.ArchTests/RepositoryTests.cs diff --git a/DomainDrivenTutorial.sln b/DomainDrivenTutorial.sln index 12a7ffe..b658a51 100644 --- a/DomainDrivenTutorial.sln +++ b/DomainDrivenTutorial.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EShoppingTutorial.Infrastru EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{81DDED9D-158B-E303-5F62-77A2896D2A5A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EShoppingTutorial.ArchTests", "EShoppingTutorial\EShoppingTutorial.ArchTests\EShoppingTutorial.ArchTests.csproj", "{7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU + {7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -87,6 +93,7 @@ Global {A12271E1-D764-4757-AFA4-203DBB555B26} = {9E43B017-E13C-453B-9C1C-A50BF9DB705F} {311A5E04-1743-4674-BE72-2A4FD050A911} = {563C32CB-B8B9-4223-823F-9F56383B2274} {F31C106C-CAA0-48B8-8046-8C2A6D394874} = {563C32CB-B8B9-4223-823F-9F56383B2274} + {7DE2D839-9AF3-4D02-9BC1-B559ECD1E31C} = {563C32CB-B8B9-4223-823F-9F56383B2274} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {58DDD7D4-5B51-440D-8E00-95270B5E1AD0} diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/ConventionTests.cs b/EShoppingTutorial/EShoppingTutorial.ArchTests/ConventionTests.cs new file mode 100644 index 0000000..c4fab59 --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/ConventionTests.cs @@ -0,0 +1,36 @@ +namespace EShoppingTutorial.ArchTests; + +public class ConventionTests +{ + private readonly System.Reflection.Assembly _applicationAssembly = typeof(Core.Application.ApplicationDependencyInjection).Assembly; + + [Fact] + public void CommandHandlers_Should_Be_Internal() + { + var result = Types.InAssembly(_applicationAssembly) + .That() + .HaveNameEndingWith("Handler") + .Should() + .NotBePublic() + .GetResult(); + + result.IsSuccessful.Should().BeTrue("because handlers contain the business execution logic and should not be exposed."); + } + + [Fact] + public void Handlers_Should_ResideIn_ApplicationNamespace_And_HaveCorrectName() + { + // Arrange & Act + var result = Types.InAssembly(_applicationAssembly) + .That() + .HaveNameEndingWith("Handler") + .Should() + .ResideInNamespace("EShoppingTutorial.Core.Application") + .GetResult(); + + // Assert + var failingTypes = string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty()); + result.IsSuccessful.Should().BeTrue( + $"because all MediatR Handlers must be located in the Application layer and follow naming conventions. Failing types: {failingTypes}"); + } +} \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/DomainRulesTests.cs b/EShoppingTutorial/EShoppingTutorial.ArchTests/DomainRulesTests.cs new file mode 100644 index 0000000..ec64311 --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/DomainRulesTests.cs @@ -0,0 +1,72 @@ +using System.Reflection; +namespace EShoppingTutorial.ArchTests; + +public class DomainRulesTests +{ + [Fact] + public void EntityProperties_Should_Not_Have_Public_Setters_Except_Init() + { + // Arrange + var domainAssembly = typeof(Core.Domain.Entities.Order).Assembly; + + // Act + var entityTypes = Types.InAssembly(domainAssembly) + .That() + .ResideInNamespace("EShoppingTutorial.Core.Domain.Entities") + .And() + .AreClasses() + .GetTypes(); + + var failingProperties = new List(); + + foreach (var type in entityTypes) + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + // If there is no setter, it's a read-only property (which is fine) + if (!property.CanWrite || property.SetMethod == null) continue; + + // Check if the setter is public + if (property.SetMethod.IsPublic) + { + // Is it a standard 'public set' or an 'init' setter? + // init setters have a special 'IsExternalInit' modifier on the return type + var isInitOnly = property.SetMethod.ReturnParameter + .GetRequiredCustomModifiers() + .Any(m => m.FullName == "System.Runtime.CompilerServices.IsExternalInit"); + + if (!isInitOnly) + { + failingProperties.Add($"{type.Name}.{property.Name}"); + } + } + } + } + + // Assert + failingProperties.Should().BeEmpty( + $"because Domain Entities should use private/protected setters for state changes to ensure encapsulation. " + + $"Standard public setters found (use 'init' for immutable properties instead): {string.Join(", ", failingProperties)}"); + } + + [Fact] + public void ValueObjects_Should_Be_Immutable() + { + // All types in ValueObjects namespace should be records or have private setters + var result = Types.InAssembly(typeof(Core.Domain.Entities.Order).Assembly) + .That() + .ResideInNamespace("EShoppingTutorial.Core.Domain.ValueObjects") + .Should() + .BeSealed() // Good practice for Value Objects + .GetResult(); + + // Assert + // We collect the names of types that failed the rule + var failingTypes = string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty()); + + result.IsSuccessful.Should().BeTrue( + $"because all Value Objects should be sealed to ensure correct equality behavior. Failing types: {failingTypes}"); + } +} \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/EShoppingTutorial.ArchTests.csproj b/EShoppingTutorial/EShoppingTutorial.ArchTests/EShoppingTutorial.ArchTests.csproj new file mode 100644 index 0000000..08cdab9 --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/EShoppingTutorial.ArchTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/GlobalUsings.cs b/EShoppingTutorial/EShoppingTutorial.ArchTests/GlobalUsings.cs new file mode 100644 index 0000000..8b00fec --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using NetArchTest.Rules; +global using FluentAssertions; \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/LayerTests.cs b/EShoppingTutorial/EShoppingTutorial.ArchTests/LayerTests.cs new file mode 100644 index 0000000..edce67f --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/LayerTests.cs @@ -0,0 +1,37 @@ +namespace EShoppingTutorial.ArchTests; + +public class LayerTests +{ + private const string ApplicationNamespace = "EShoppingTutorial.Core.Application"; + private const string InfrastructureNamespace = "EShoppingTutorial.Infrastructure"; + private const string PersistenceNamespace = "EShoppingTutorial.Core.Persistence"; + + [Fact] + public void Domain_Should_Not_Have_Dependency_On_Other_Layers() + { + var assembly = typeof(Core.Domain.Entities.Order).Assembly; + + var result = Types.InAssembly(assembly) + .ShouldNot() + .HaveDependencyOnAll(ApplicationNamespace, InfrastructureNamespace, PersistenceNamespace) + .GetResult(); + + result.IsSuccessful.Should().BeTrue($"Domain layer must be pure. Failures: {GetFailingTypes(result)}"); + } + + [Fact] + public void Application_Should_Not_Have_Dependency_On_Infrastructure_Or_Persistence() + { + var assembly = typeof(Core.Application.Orders.Commands.CreateOrder.CreateOrderCommand).Assembly; + + var result = Types.InAssembly(assembly) + .ShouldNot() + .HaveDependencyOnAll(InfrastructureNamespace, PersistenceNamespace) + .GetResult(); + + result.IsSuccessful.Should().BeTrue($"Application layer should only depend on Domain. Failures: {GetFailingTypes(result)}"); + } + + private string GetFailingTypes(TestResult result) => + string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty()); +} \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.ArchTests/RepositoryTests.cs b/EShoppingTutorial/EShoppingTutorial.ArchTests/RepositoryTests.cs new file mode 100644 index 0000000..af2788b --- /dev/null +++ b/EShoppingTutorial/EShoppingTutorial.ArchTests/RepositoryTests.cs @@ -0,0 +1,49 @@ +namespace EShoppingTutorial.ArchTests; + +public class RepositoryTests +{ + [Fact] + public void RepositoryInterfaces_Should_Be_In_Domain_Repositories_Namespace() + { + var _domainAssembly = typeof(Core.Domain.IUnitOfWork).Assembly; + + // Rule: If it's an interface and ends in 'Repository', it MUST be in the correct namespace + var result = Types.InAssembly(_domainAssembly) + .That() + .AreClasses() + .And() + .HaveNameEndingWith("Repository") + .Should() + // This is a "fail-fast" rule: + // We say they should be in a namespace that DOES NOT exist in this project + .ResideInNamespace("EShoppingTutorial.Core.Persistence") + .GetResult(); + + // Assert + result.IsSuccessful.Should().BeTrue( + $"Architecture Violation: Domain should only contain interfaces. " + + $"Implementations Repository Classes found: {string.Join(", ", result.FailingTypeNames ?? [])}"); + } + + [Fact] + public void Persistence_Should_Not_Contain_Any_Interfaces() + { + // Arrange + var persistenceAssembly = typeof(Core.Persistence.UnitOfWork).Assembly; + + // Act: Scan for ANY interface in the Persistence project + var result = Types.InAssembly(persistenceAssembly) + .That() + .AreInterfaces() + .Should() + // This is a "fail-fast" rule: + // We say they should be in a namespace that DOES NOT exist in this project + .ResideInNamespace("EShoppingTutorial.Core.Domain") + .GetResult(); + + // Assert + result.IsSuccessful.Should().BeTrue( + $"Architecture Violation: Persistence should only contain implementations (Classes). " + + $"Interfaces found: {string.Join(", ", result.FailingTypeNames ?? [])}"); + } +} \ No newline at end of file diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs index a462524..65d6fd6 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Application.Orders.Commands.CreateOrder; -public class CreateOrderHandler(IUnitOfWork unitOfWork, +internal class CreateOrderHandler(IUnitOfWork unitOfWork, ITaxCalculationService taxCalculationService, IMapper mapper) : IRequestHandler diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/DeleteOrder/DeleteOrderHandler.cs b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/DeleteOrder/DeleteOrderHandler.cs index 6465a3a..ce13e02 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/DeleteOrder/DeleteOrderHandler.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Commands/DeleteOrder/DeleteOrderHandler.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Application.Orders.Commands.DeleteOrder; -public class DeleteOrderHandler(IUnitOfWork unitOfWork) : IRequestHandler +internal class DeleteOrderHandler(IUnitOfWork unitOfWork) : IRequestHandler { public async Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) { diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetAllOrders/GetAllOrdersHandler.cs b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetAllOrders/GetAllOrdersHandler.cs index 9a5655f..87600c4 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetAllOrders/GetAllOrdersHandler.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetAllOrders/GetAllOrdersHandler.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Application.Orders.Queries.GetAllOrders; -public class GetAllOrdersHandler(IUnitOfWork unitOfWork, IMapper mapper) +internal class GetAllOrdersHandler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler> { public async Task> Handle(GetAllOrdersQuery request, CancellationToken ct) diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetOrderById/GetOrderByIdHandler.cs b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetOrderById/GetOrderByIdHandler.cs index 1b03ac5..bae2826 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetOrderById/GetOrderByIdHandler.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetOrderById/GetOrderByIdHandler.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Application.Orders.Queries.GetOrderById; -public class GetOrderByIdHandler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler +internal class GetOrderByIdHandler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler { public async Task Handle(GetOrderByIdQuery request, CancellationToken ct) { diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetPagedOrders/GetPagedOrdersHandler.cs b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetPagedOrders/GetPagedOrdersHandler.cs index 7350975..4d9ac5a 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetPagedOrders/GetPagedOrdersHandler.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core.Application/Orders/Queries/GetPagedOrders/GetPagedOrdersHandler.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Application.Orders.Queries.GetPagedOrders; -public class GetPagedOrdersHandler(IUnitOfWork unitOfWork, IMapper mapper) +internal class GetPagedOrdersHandler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler> { public async Task> Handle(GetPagedOrdersQuery request, CancellationToken ct) diff --git a/EShoppingTutorial/EShoppingTutorial.Core.Persistence/EShoppingTutorial.Core.Persistence.csproj b/EShoppingTutorial/EShoppingTutorial.Core.Persistence/EShoppingTutorial.Core.Persistence.csproj index db2e34f..335e636 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core.Persistence/EShoppingTutorial.Core.Persistence.csproj +++ b/EShoppingTutorial/EShoppingTutorial.Core.Persistence/EShoppingTutorial.Core.Persistence.csproj @@ -13,6 +13,10 @@ <_Parameter1>EShoppingTutorial.UnitTests + + + <_Parameter1>EShoppingTutorial.ArchTests + diff --git a/EShoppingTutorial/EShoppingTutorial.Core/Entities/Order.cs b/EShoppingTutorial/EShoppingTutorial.Core/Entities/Order.cs index 1f3e641..3bfd212 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core/Entities/Order.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core/Entities/Order.cs @@ -16,7 +16,7 @@ public class Order : IAggregateRoot // Expose as IReadOnlyCollection to prevent external tampering private readonly List _orderItems = []; - public ICollection OrderItems => _orderItems.AsReadOnly(); + public IEnumerable OrderItems => _orderItems.AsReadOnly(); // EF Core requires a parameterless constructor protected Order() { } diff --git a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Address.cs b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Address.cs index 0571bcb..1d47dd2 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Address.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Address.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Domain.ValueObjects; -public record Address +public sealed record Address { public string Street { get; init; } = default!; public string City { get; init; } = default!; diff --git a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Price.cs b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Price.cs index 46bcb65..7bfe893 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Price.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/Price.cs @@ -1,6 +1,6 @@ namespace EShoppingTutorial.Core.Domain.ValueObjects; -public record Price +public sealed record Price { public decimal Value { get; init; } public Currency Currency { get; init; } = Currency.Unspecified; diff --git a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/StronglyTypedIDs.cs b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/StronglyTypedIDs.cs index 7d56f88..2d0188b 100644 --- a/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/StronglyTypedIDs.cs +++ b/EShoppingTutorial/EShoppingTutorial.Core/ValueObjects/StronglyTypedIDs.cs @@ -3,13 +3,13 @@ namespace EShoppingTutorial.Core.Domain.ValueObjects; [NotMapped] -public record OrderId(int Value); +public sealed record OrderId(int Value); [NotMapped] -public record OrderItemId(int Value); +public sealed record OrderItemId(int Value); [NotMapped] -public record ProductId(int Value); +public sealed record ProductId(int Value); [NotMapped] -public record CustomerId(int Value); +public sealed record CustomerId(int Value); diff --git a/EShoppingTutorial/EShoppingTutorial.UnitTests/Domain/Entities/OrderUnitTests.cs b/EShoppingTutorial/EShoppingTutorial.UnitTests/Domain/Entities/OrderUnitTests.cs index e46ff25..57e5696 100644 --- a/EShoppingTutorial/EShoppingTutorial.UnitTests/Domain/Entities/OrderUnitTests.cs +++ b/EShoppingTutorial/EShoppingTutorial.UnitTests/Domain/Entities/OrderUnitTests.cs @@ -6,12 +6,15 @@ public class OrderUnitTests [Test] public void InstantiatingOrder_WithEmptyOrderItems_ExpectsBusinessRuleBrokenException() { - // arrange & act - static void testDelegate() => new Order(new CustomerId(1), Mock.Of
(), orderItems: []); + var dummyAddress = new Address("Street", "City", "Country", "ZipCode"); + var customerId = new CustomerId(1); - // assert + // Act + TestDelegate testDelegate = () => new Order(customerId, dummyAddress, orderItems: []); + + // Assert var ex = Assert.Throws(testDelegate); - Assert.That(ex.Message.Contains("Order must have at least one item", StringComparison.CurrentCultureIgnoreCase)); + ex.Message.Should().Contain("Order must have at least one item"); } [Test] @@ -22,10 +25,12 @@ public void OrderItemsProperty_AddingOrderItemToReadOnlyCollection_ExpectsNotSup var priceMock = new Price(1, Currency.USD); var orderItemMock = new OrderItem(productIdMock, priceMock); - var order = new Order(new CustomerId(1), Mock.Of
(), [orderItemMock]); + var dummyAddress = new Address("Street", "City", "Country", "ZipCode"); + var order = new Order(new CustomerId(1), dummyAddress, [orderItemMock]); // act - void testDelegate() => order.OrderItems.Add(orderItemMock); + var orderItems = order.OrderItems as ICollection; + void testDelegate() => orderItems.Add(orderItemMock); // assert var ex = Assert.Throws(testDelegate); diff --git a/EShoppingTutorial/EShoppingTutorial.UnitTests/EShoppingTutorial.UnitTests.csproj b/EShoppingTutorial/EShoppingTutorial.UnitTests/EShoppingTutorial.UnitTests.csproj index d1634d0..3b4231b 100644 --- a/EShoppingTutorial/EShoppingTutorial.UnitTests/EShoppingTutorial.UnitTests.csproj +++ b/EShoppingTutorial/EShoppingTutorial.UnitTests/EShoppingTutorial.UnitTests.csproj @@ -11,6 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/EShoppingTutorial/EShoppingTutorial.UnitTests/GlobalUsings.cs b/EShoppingTutorial/EShoppingTutorial.UnitTests/GlobalUsings.cs index 63a3244..40c0958 100644 --- a/EShoppingTutorial/EShoppingTutorial.UnitTests/GlobalUsings.cs +++ b/EShoppingTutorial/EShoppingTutorial.UnitTests/GlobalUsings.cs @@ -8,6 +8,7 @@ global using NUnit.Framework; global using Moq; global using Microsoft.EntityFrameworkCore; +global using FluentAssertions; // Domain Specific Namespaces global using EShoppingTutorial.Core.Domain.Entities; diff --git a/EShoppingTutorial/EShoppingTutorial.UnitTests/Repositories/OrderRepositoryIntegrationTests.cs b/EShoppingTutorial/EShoppingTutorial.UnitTests/Repositories/OrderRepositoryIntegrationTests.cs index 80e4ceb..a0d34ee 100644 --- a/EShoppingTutorial/EShoppingTutorial.UnitTests/Repositories/OrderRepositoryIntegrationTests.cs +++ b/EShoppingTutorial/EShoppingTutorial.UnitTests/Repositories/OrderRepositoryIntegrationTests.cs @@ -23,7 +23,8 @@ public async Task MethodAdd_TrackingNumber_MustNotBeNull() { // arrange ProductId productId = new(1); - var order = new Order(new CustomerId(1), Mock.Of
(), [new OrderItem (productId, new Price(value: 2000, Currency.EUR))]); + var dummyAddress = new Address("Street", "City", "Country", "ZipCode"); + var order = new Order(new CustomerId(1), dummyAddress, [new OrderItem (productId, new Price(value: 2000, Currency.EUR))]); // act _orderRepository.Add(order);