From 8248af19530e5afa870d977fb087d827337d05a9 Mon Sep 17 00:00:00 2001 From: giovano <135080375+giovanoh@users.noreply.github.com> Date: Wed, 21 May 2025 00:24:25 -0300 Subject: [PATCH] test: add and improve unit/integration tests - add unit tests for event consumers - add integration tests for CheckoutController - add ServiceTestBase to centralize logger verification in service tests - update TestDataHelper and integration test fixtures for better test isolation - remove unused PaymentRequestedEvent - minor fixes in BookOrderRepository and BookOrderService for testability --- .../Repositories/BookOrderRepository.cs | 2 +- .../Services/BookOrderService.cs | 10 + .../Consumers/OrderCompletedConsumer.cs | 1 + .../Consumers/OrderDeliveredConsumer.cs | 1 + .../Consumers/OrderProcessingConsumer.cs | 1 + .../Consumers/OrderShippedConsumer.cs | 1 + .../Consumers/PaymentConfirmedConsumer.cs | 1 + .../Consumers/PaymentFailedConsumer.cs | 1 + .../Messages/PaymentRequestedEvent.cs | 8 - .../Api/CheckoutControllerTest.cs | 180 ++++++++++++++++++ .../Api/IntegrationTestBase.cs | 25 ++- .../Fixtures/LibraryApiFactory.cs | 19 +- .../Fixtures/TestDataHelper.cs | 45 +++++ .../Library.API.IntegrationTests.csproj | 11 +- .../Consumers/OrderCompletedConsumerTests.cs | 38 ++++ .../Consumers/OrderDeliveredConsumerTests.cs | 38 ++++ .../Consumers/OrderProcessingConsumerTests.cs | 38 ++++ .../Consumers/OrderShippedConsumerTests.cs | 38 ++++ .../PaymentConfirmedConsumerTests.cs | 38 ++++ .../Consumers/PaymentFailedConsumerTests.cs | 38 ++++ .../Extensions/EnumExtensionsTests.cs | 13 ++ .../Services/AuthorServiceTests.cs | 95 +-------- .../Services/BookOrderServiceTests.cs | 79 +++----- .../Services/BookServiceTests.cs | 105 ++-------- .../Services/ServiceTestBase.cs | 24 +++ 25 files changed, 599 insertions(+), 251 deletions(-) delete mode 100644 src/Library.Events/Messages/PaymentRequestedEvent.cs create mode 100644 tests/Library.API.IntegrationTests/Api/CheckoutControllerTest.cs create mode 100644 tests/Library.API.Tests/Consumers/OrderCompletedConsumerTests.cs create mode 100644 tests/Library.API.Tests/Consumers/OrderDeliveredConsumerTests.cs create mode 100644 tests/Library.API.Tests/Consumers/OrderProcessingConsumerTests.cs create mode 100644 tests/Library.API.Tests/Consumers/OrderShippedConsumerTests.cs create mode 100644 tests/Library.API.Tests/Consumers/PaymentConfirmedConsumerTests.cs create mode 100644 tests/Library.API.Tests/Consumers/PaymentFailedConsumerTests.cs create mode 100644 tests/Library.API.Tests/Services/ServiceTestBase.cs diff --git a/src/Library.API/Infrastructure/Repositories/BookOrderRepository.cs b/src/Library.API/Infrastructure/Repositories/BookOrderRepository.cs index 27c25d6..1cb51fa 100644 --- a/src/Library.API/Infrastructure/Repositories/BookOrderRepository.cs +++ b/src/Library.API/Infrastructure/Repositories/BookOrderRepository.cs @@ -1,4 +1,5 @@ using System.Diagnostics; + using Microsoft.EntityFrameworkCore; using Library.API.Domain.Models; @@ -22,7 +23,6 @@ public async Task AddAsync(BookOrder bookOrder) { using var activity = _activitySource.StartActivity("Repository: BookOrderRepository.AddAsync"); await _context.BookOrders.AddAsync(bookOrder); - await _context.SaveChangesAsync(); } public async Task FindByIdAsync(int id) diff --git a/src/Library.API/Infrastructure/Services/BookOrderService.cs b/src/Library.API/Infrastructure/Services/BookOrderService.cs index 836a68f..8c65c0a 100644 --- a/src/Library.API/Infrastructure/Services/BookOrderService.cs +++ b/src/Library.API/Infrastructure/Services/BookOrderService.cs @@ -16,12 +16,14 @@ namespace Library.API.Infrastructure.Services; public class BookOrderService : BaseService, IBookOrderService { private readonly IBookOrderRepository _bookOrderRepository; + private readonly IBookRepository _bookRepository; private readonly ActivitySource _activitySource; private readonly IPublishEndpoint _publishEndpoint; private readonly IMapper _mapper; public BookOrderService( IBookOrderRepository bookOrderRepository, + IBookRepository bookRepository, IUnitOfWork unitOfWork, ILogger logger, ActivitySource activitySource, @@ -30,6 +32,7 @@ public BookOrderService( : base(unitOfWork, logger) { _bookOrderRepository = bookOrderRepository; + _bookRepository = bookRepository; _activitySource = activitySource; _mapper = mapper; _publishEndpoint = publishEndpoint; @@ -62,6 +65,13 @@ public async Task AddAsync(BookOrder bookOrder) if (bookOrder.Items.Count == 0) return ServiceResponse.Fail("Book order must have at least one item", ErrorType.ValidationError); + foreach (var item in bookOrder.Items) + { + var book = await _bookRepository.FindByIdAsync(item.BookId); + if (book == null) + return ServiceResponse.Fail($"Book with id {item.BookId} was not found", ErrorType.ValidationError); + } + await _bookOrderRepository.AddAsync(bookOrder); await _unitOfWork.CompleteAsync(); diff --git a/src/Library.API/Infrastructure/Services/Consumers/OrderCompletedConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/OrderCompletedConsumer.cs index 15c8fe3..c86f739 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/OrderCompletedConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/OrderCompletedConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.API/Infrastructure/Services/Consumers/OrderDeliveredConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/OrderDeliveredConsumer.cs index 823e22c..ba03e9f 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/OrderDeliveredConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/OrderDeliveredConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.API/Infrastructure/Services/Consumers/OrderProcessingConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/OrderProcessingConsumer.cs index f02e0e3..7061970 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/OrderProcessingConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/OrderProcessingConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.API/Infrastructure/Services/Consumers/OrderShippedConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/OrderShippedConsumer.cs index de9c252..b629fe0 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/OrderShippedConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/OrderShippedConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.API/Infrastructure/Services/Consumers/PaymentConfirmedConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/PaymentConfirmedConsumer.cs index dcd7480..fca8c56 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/PaymentConfirmedConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/PaymentConfirmedConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.API/Infrastructure/Services/Consumers/PaymentFailedConsumer.cs b/src/Library.API/Infrastructure/Services/Consumers/PaymentFailedConsumer.cs index d7be952..2270fb2 100644 --- a/src/Library.API/Infrastructure/Services/Consumers/PaymentFailedConsumer.cs +++ b/src/Library.API/Infrastructure/Services/Consumers/PaymentFailedConsumer.cs @@ -1,4 +1,5 @@ using MassTransit; + using Library.API.Domain.Models; using Library.API.Domain.Services; using Library.Events.Messages; diff --git a/src/Library.Events/Messages/PaymentRequestedEvent.cs b/src/Library.Events/Messages/PaymentRequestedEvent.cs deleted file mode 100644 index 522b24c..0000000 --- a/src/Library.Events/Messages/PaymentRequestedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Library.Events.Messages; - -public class PaymentRequestedEvent -{ - public int OrderId { get; set; } - public decimal Amount { get; set; } - public DateTime RequestedAt { get; set; } -} \ No newline at end of file diff --git a/tests/Library.API.IntegrationTests/Api/CheckoutControllerTest.cs b/tests/Library.API.IntegrationTests/Api/CheckoutControllerTest.cs new file mode 100644 index 0000000..5622e86 --- /dev/null +++ b/tests/Library.API.IntegrationTests/Api/CheckoutControllerTest.cs @@ -0,0 +1,180 @@ +using System.Net; +using System.Net.Http.Json; + +using FluentAssertions; + +using Library.API.DTOs; +using Library.API.DTOs.Response; +using Library.API.IntegrationTests.Fixtures; +using Library.Events.Messages; + +namespace Library.API.IntegrationTests.Api; + +public class CheckoutControllerTest(LibraryApiFactory factory) : IntegrationTestBase(factory) +{ + + [Fact] + public async Task CreateBookOrder_ShouldReturnSuccess_AndPublishOrderPlacedEvent() + { + // Arrange + TestDataHelper.SeedAuthors(Factory, true); + TestDataHelper.SeedBooks(Factory); + var endpoint = "/api/checkout"; + var newBookOrder = new SaveBookOrderDto + { + Items = [ + new SaveBookOrderItemDto { BookId = 1, Quantity = 1 } + ] + }; + + // Act + var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync>(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + apiResponse.Should().NotBeNull(); + apiResponse.Data.Should().NotBeNull(); + apiResponse.Data.Id.Should().Be(1); + apiResponse.Data.Items.Should().HaveCount(1); + apiResponse.Data.Items[0].BookId.Should().Be(1); + apiResponse.Data.Items[0].Quantity.Should().Be(1); + + VerifyEventPublished(); + } + + [Fact] + public async Task CreateBookOrder_WithInvalidData_ShouldReturnBadRequest_AndNotPublishEvent() + { + // Arrange + var endpoint = "/api/checkout"; + var newBookOrder = new SaveBookOrderDto { Items = [] }; + + // Act + var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + apiResponse.Should().NotBeNull(); + apiResponse.Title.Should().Be("Validation Error"); + apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest); + apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1"); + apiResponse.Detail.Should().Be("Book order must have at least one item"); + apiResponse.Instance.Should().Be(endpoint); + + VerifyEventNotPublished(); + } + + [Fact] + public async Task CreateBookOrder_WithInvalidBookId_ShouldReturnBadRequest() + { + // Arrange + var endpoint = "/api/checkout"; + var newBookOrder = new SaveBookOrderDto + { + Items = [ + new SaveBookOrderItemDto { BookId = 999, Quantity = 1 } + ] + }; + + // Act + var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + apiResponse.Should().NotBeNull(); + apiResponse.Title.Should().Be("Validation Error"); + apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest); + apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1"); + apiResponse.Detail.Should().Be("Book with id 999 was not found"); + apiResponse.Instance.Should().Be(endpoint); + + VerifyEventNotPublished(); + } + + [Fact] + public async Task CreateBookOrder_WithInvalidQuantity_ShouldReturnBadRequest() + { + // Arrange + var endpoint = "/api/checkout"; + var newBookOrder = new SaveBookOrderDto + { + Items = [ + new SaveBookOrderItemDto { BookId = 1 } + ] + }; + + // Act + var response = await Client.PostAsJsonAsync(endpoint, newBookOrder, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + apiResponse.Should().NotBeNull(); + apiResponse.Title.Should().Be("Validation Error"); + apiResponse.Status.Should().Be((int)HttpStatusCode.BadRequest); + apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1"); + apiResponse.Detail.Should().Be("One or more validation errors occurred."); + apiResponse.Instance.Should().Be(endpoint); + apiResponse.Errors.Should().NotBeNull(); + apiResponse.Errors.Should().HaveCount(1); + apiResponse.Errors.Should().ContainKey("Items[0].Quantity"); + apiResponse.Errors["Items[0].Quantity"].Should().Contain("The field Quantity must be between 1 and 50."); + + VerifyEventNotPublished(); + } + + [Fact] + public async Task GetBookOrder_WithValidId_ShouldReturnBookOrder() + { + // Arrange + TestDataHelper.SeedAuthors(Factory, true); + TestDataHelper.SeedBooks(Factory); + TestDataHelper.SeedBookOrders(Factory); + var orderId = 1; + var endpoint = $"/api/checkout/{orderId}"; + + // Act + var response = await Client.GetAsync(endpoint, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync>(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + apiResponse.Should().NotBeNull(); + apiResponse.Data.Should().NotBeNull(); + apiResponse.Data.Id.Should().Be(orderId); + apiResponse.Data.CheckoutDate.Should().Be(new DateTime(2020, 1, 1)); + apiResponse.Data.Status.Should().Be("Order Placed"); + apiResponse.Data.Items.Should().HaveCount(1); + apiResponse.Data.Items[0].BookId.Should().Be(1); + apiResponse.Data.Items[0].Title.Should().Be("Book 1"); + apiResponse.Data.Items[0].Quantity.Should().Be(1); + + VerifyEventNotPublished(); + } + + [Fact] + public async Task GetBookOrder_WithInvalidId_ShouldReturnNotFound() + { + // Arrange + var endpoint = "/api/checkout/999"; + + // Act + var response = await Client.GetAsync(endpoint, CancellationToken); + var apiResponse = await response.Content.ReadFromJsonAsync(CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + apiResponse.Should().NotBeNull(); + apiResponse.Title.Should().Be("Resource Not Found"); + apiResponse.Status.Should().Be((int)HttpStatusCode.NotFound); + apiResponse.Type.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); + apiResponse.Detail.Should().Be("Book order with id 999 was not found"); + apiResponse.Instance.Should().Be(endpoint); + + VerifyEventNotPublished(); + } + +} \ No newline at end of file diff --git a/tests/Library.API.IntegrationTests/Api/IntegrationTestBase.cs b/tests/Library.API.IntegrationTests/Api/IntegrationTestBase.cs index e897982..f943858 100644 --- a/tests/Library.API.IntegrationTests/Api/IntegrationTestBase.cs +++ b/tests/Library.API.IntegrationTests/Api/IntegrationTestBase.cs @@ -1,19 +1,23 @@ -using Library.API.IntegrationTests.Fixtures; - +using MassTransit; using Microsoft.AspNetCore.Mvc.Testing; +using Moq; + +using Library.API.IntegrationTests.Fixtures; namespace Library.API.IntegrationTests.Api; -public class IntegrationTestBase : IClassFixture, IAsyncLifetime +public abstract class IntegrationTestBase : IClassFixture, IAsyncLifetime { protected readonly HttpClient Client; protected readonly LibraryApiFactory Factory; + protected readonly Mock PublishEndpointMock; private CancellationTokenSource? _cts; private const int TestTimeoutSeconds = 10; protected IntegrationTestBase(LibraryApiFactory factory) { Factory = factory; + PublishEndpointMock = factory.GetPublishEndpointMock(); Client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false @@ -25,6 +29,7 @@ protected IntegrationTestBase(LibraryApiFactory factory) public ValueTask InitializeAsync() { _cts = new CancellationTokenSource(TimeSpan.FromSeconds(TestTimeoutSeconds)); + PublishEndpointMock.Reset(); return ValueTask.CompletedTask; } @@ -34,4 +39,18 @@ public ValueTask DisposeAsync() _cts?.Dispose(); return ValueTask.CompletedTask; } + + protected void VerifyEventPublished(Times? times = null) where T : class + { + PublishEndpointMock.Verify( + x => x.Publish(It.IsAny(), It.IsAny()), + times ?? Times.Once()); + } + + protected void VerifyEventNotPublished() where T : class + { + PublishEndpointMock.Verify( + x => x.Publish(It.IsAny(), It.IsAny()), + Times.Never()); + } } \ No newline at end of file diff --git a/tests/Library.API.IntegrationTests/Fixtures/LibraryApiFactory.cs b/tests/Library.API.IntegrationTests/Fixtures/LibraryApiFactory.cs index 03a4141..16c0e60 100644 --- a/tests/Library.API.IntegrationTests/Fixtures/LibraryApiFactory.cs +++ b/tests/Library.API.IntegrationTests/Fixtures/LibraryApiFactory.cs @@ -4,6 +4,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using MassTransit; +using Moq; + using Library.API.Infrastructure.Contexts; namespace Library.API.IntegrationTests.Fixtures; @@ -11,22 +14,32 @@ namespace Library.API.IntegrationTests.Fixtures; public class LibraryApiFactory : WebApplicationFactory { private readonly string _databaseName = $"library_api_test_db_{Guid.NewGuid()}"; + private Mock _publishEndpointMock = new(); + + public Mock GetPublishEndpointMock() => _publishEndpointMock; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureServices(services => { - var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ApiDbContext)); - if (descriptor != null) + var dbDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ApiDbContext)); + if (dbDescriptor != null) { - services.Remove(descriptor); + services.Remove(dbDescriptor); services.AddDbContext(options => { options.UseInMemoryDatabase(_databaseName); }); } + var massTransitDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IPublishEndpoint)); + if (massTransitDescriptor != null) + { + services.Remove(massTransitDescriptor); + services.AddSingleton(_publishEndpointMock.Object); + } + var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); diff --git a/tests/Library.API.IntegrationTests/Fixtures/TestDataHelper.cs b/tests/Library.API.IntegrationTests/Fixtures/TestDataHelper.cs index ae9fcac..453afdf 100644 --- a/tests/Library.API.IntegrationTests/Fixtures/TestDataHelper.cs +++ b/tests/Library.API.IntegrationTests/Fixtures/TestDataHelper.cs @@ -101,6 +101,51 @@ private static void SeedBooksInternal(ApiDbContext dbContext, Book[] books, bool dbContext.SaveChanges(); } + private static BookOrder[] GetDefaultBookOrders() => + [ + new BookOrder + { + Id = 1, + CheckoutDate = new DateTime(2020, 1, 1), + Status = BookOrderStatus.Placed, + Items = [ + new BookOrderItem + { + Id = 1, + BookId = 1, + Quantity = 1, + } + ] + } + ]; + + public static void SeedBookOrders(LibraryApiFactory factory, bool resetDatabase = false) + { + var bookOrders = GetDefaultBookOrders(); + WithDbContext(factory, dbContext => + { + SeedBookOrdersInternal(dbContext, bookOrders, resetDatabase); + }); + } + + public static void SeedBookOrders(ApiDbContext dbContext, bool resetDatabase = false) + { + var bookOrders = GetDefaultBookOrders(); + SeedBookOrdersInternal(dbContext, bookOrders, resetDatabase); + } + + private static void SeedBookOrdersInternal(ApiDbContext dbContext, BookOrder[] bookOrders, bool resetDatabase = false) + { + if (resetDatabase) + { + dbContext.Database.EnsureDeleted(); + } + dbContext.Database.EnsureCreated(); + + dbContext.BookOrders.AddRange(bookOrders); + dbContext.SaveChanges(); + } + private static void WithDbContext(LibraryApiFactory factory, Action action) { using IServiceScope scope = factory.Services.CreateScope(); diff --git a/tests/Library.API.IntegrationTests/Library.API.IntegrationTests.csproj b/tests/Library.API.IntegrationTests/Library.API.IntegrationTests.csproj index 1e4ecc9..be3dce7 100644 --- a/tests/Library.API.IntegrationTests/Library.API.IntegrationTests.csproj +++ b/tests/Library.API.IntegrationTests/Library.API.IntegrationTests.csproj @@ -27,19 +27,20 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - + + diff --git a/tests/Library.API.Tests/Consumers/OrderCompletedConsumerTests.cs b/tests/Library.API.Tests/Consumers/OrderCompletedConsumerTests.cs new file mode 100644 index 0000000..2c4a679 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/OrderCompletedConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class OrderCompletedConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly OrderCompletedConsumer _consumer; + + public OrderCompletedConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new OrderCompletedConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenOrderCompleted() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new OrderCompletedEvent { OrderId = 123, CompletedAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.Completed), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Consumers/OrderDeliveredConsumerTests.cs b/tests/Library.API.Tests/Consumers/OrderDeliveredConsumerTests.cs new file mode 100644 index 0000000..33d4d21 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/OrderDeliveredConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class OrderDeliveredConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly OrderDeliveredConsumer _consumer; + + public OrderDeliveredConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new OrderDeliveredConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenOrderDelivered() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new OrderDeliveredEvent { OrderId = 123, DeliveredAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.Delivered), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Consumers/OrderProcessingConsumerTests.cs b/tests/Library.API.Tests/Consumers/OrderProcessingConsumerTests.cs new file mode 100644 index 0000000..3406ce4 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/OrderProcessingConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class OrderProcessingConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly OrderProcessingConsumer _consumer; + + public OrderProcessingConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new OrderProcessingConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenOrderProcessing() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new OrderProcessingEvent { OrderId = 123, ProcessingAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.Processing), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Consumers/OrderShippedConsumerTests.cs b/tests/Library.API.Tests/Consumers/OrderShippedConsumerTests.cs new file mode 100644 index 0000000..c257d39 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/OrderShippedConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class OrderShippedConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly OrderShippedConsumer _consumer; + + public OrderShippedConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new OrderShippedConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenOrderShipped() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new OrderShippedEvent { OrderId = 123, ShippedAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.Shipped), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Consumers/PaymentConfirmedConsumerTests.cs b/tests/Library.API.Tests/Consumers/PaymentConfirmedConsumerTests.cs new file mode 100644 index 0000000..ba762a7 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/PaymentConfirmedConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class PaymentConfirmedConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly PaymentConfirmedConsumer _consumer; + + public PaymentConfirmedConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new PaymentConfirmedConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenPaymentConfirmed() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new PaymentConfirmedEvent { OrderId = 123, ConfirmedAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.PaymentConfirmed), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Consumers/PaymentFailedConsumerTests.cs b/tests/Library.API.Tests/Consumers/PaymentFailedConsumerTests.cs new file mode 100644 index 0000000..5ed7ae9 --- /dev/null +++ b/tests/Library.API.Tests/Consumers/PaymentFailedConsumerTests.cs @@ -0,0 +1,38 @@ +using MassTransit; +using Moq; + +using Library.API.Domain.Models; +using Library.API.Domain.Services; +using Library.API.Infrastructure.Services.Consumers; +using Library.Events.Messages; + +namespace Library.API.Tests.Consumers; + +public class PaymentFailedConsumerTests +{ + + private readonly Mock _bookOrderServiceMock; + private readonly Mock> _contextMock; + private readonly PaymentFailedConsumer _consumer; + + public PaymentFailedConsumerTests() + { + _bookOrderServiceMock = new Mock(); + _contextMock = new Mock>(); + _consumer = new PaymentFailedConsumer(_bookOrderServiceMock.Object); + } + + [Fact] + public async Task Consume_ShouldUpdateBookOrderStatus_WhenPaymentFailed() + { + // Arrange + _contextMock.Setup(c => c.Message).Returns(new PaymentFailedEvent { OrderId = 123, Reason = "Insufficient funds", FailedAt = new DateTime(2025, 5, 1) }); + + // Act + await _consumer.Consume(_contextMock.Object); + + // Assert + _bookOrderServiceMock.Verify(x => x.UpdateStatusAsync(123, BookOrderStatus.PaymentFailed), Times.Once); + } + +} diff --git a/tests/Library.API.Tests/Extensions/EnumExtensionsTests.cs b/tests/Library.API.Tests/Extensions/EnumExtensionsTests.cs index af6bd36..8fafe03 100644 --- a/tests/Library.API.Tests/Extensions/EnumExtensionsTests.cs +++ b/tests/Library.API.Tests/Extensions/EnumExtensionsTests.cs @@ -52,5 +52,18 @@ public void ToDescription_ShouldReturnEnumName_WhenEnumHasNoDescription() secondDescription.Should().Be("Second"); thirdDescription.Should().Be("Third"); } + + [Fact] + public void ToDescription_ShouldReturnValueToString_WhenEnumValueIsInvalid() + { + // Arrange + var invalidValue = (TestEnumWithoutDescription)999; + + // Act + var description = invalidValue.ToDescription(); + + // Assert + description.Should().Be("999"); + } } diff --git a/tests/Library.API.Tests/Services/AuthorServiceTests.cs b/tests/Library.API.Tests/Services/AuthorServiceTests.cs index 88ca075..2d2aa97 100644 --- a/tests/Library.API.Tests/Services/AuthorServiceTests.cs +++ b/tests/Library.API.Tests/Services/AuthorServiceTests.cs @@ -2,7 +2,6 @@ using FluentAssertions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using Moq; using Library.API.Domain.Models; @@ -13,22 +12,20 @@ namespace Library.API.Tests.Services; -public class AuthorServiceTests +public class AuthorServiceTests : ServiceTestBase { private readonly Mock _authorRepositoryMock; private readonly Mock _unitOfWorkMock; - private readonly Mock> _loggerMock; private readonly ActivitySource _activitySource; public AuthorServiceTests() { _authorRepositoryMock = new Mock(); _unitOfWorkMock = new Mock(); - _loggerMock = new Mock>(); _activitySource = new ActivitySource("Library.API.Tests"); } private AuthorService CreateService() => - new AuthorService(_authorRepositoryMock.Object, _unitOfWorkMock.Object, _loggerMock.Object, _activitySource); + new AuthorService(_authorRepositoryMock.Object, _unitOfWorkMock.Object, LoggerMock.Object, _activitySource); [Fact] public async Task ListAsync_ShouldReturnAuthors_WhenSuccessful() @@ -71,16 +68,7 @@ public async Task ListAsync_ShouldReturnError_WhenRepositoryFails() result.Message.Should().Be("An error occurred while retrieving the authors"); _authorRepositoryMock.Verify(repo => repo.ListAsync(), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while listing authors")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while listing authors"); } [Fact] @@ -147,16 +135,7 @@ public async Task FindByIdAsync_ShouldReturnError_WhenRepositoryFails() result.Message.Should().Be("An error occurred while retrieving the author"); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(author.Id), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains($"Error occurred while finding author {author.Id}")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while finding author"); } [Fact] @@ -204,16 +183,7 @@ public async Task AddAsync_ShouldReturnError_WhenRepositoryFails() _authorRepositoryMock.Verify(repo => repo.AddAsync(author), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while saving author")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while saving author"); } [Fact] @@ -237,16 +207,7 @@ public async Task AddAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _authorRepositoryMock.Verify(repo => repo.AddAsync(author), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Unexpected error occurred while adding author.")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Unexpected error occurred while adding author."); } [Fact] @@ -322,16 +283,7 @@ public async Task UpdateAsync_ShouldReturnError_WhenRepositoryFails() _authorRepositoryMock.Verify(repo => repo.Update(author), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(author.Id), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while updating author")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while updating author"); } [Fact] @@ -356,16 +308,7 @@ public async Task UpdateAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _authorRepositoryMock.Verify(repo => repo.Update(author), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Unexpected error occurred while updating author")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog($"Unexpected error occurred while updating author {author.Id}."); } [Fact] @@ -441,16 +384,7 @@ public async Task DeleteAsync_ShouldReturnError_WhenRepositoryFails() _authorRepositoryMock.Verify(repo => repo.Delete(author), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(author.Id), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while deleting author")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while deleting author"); } [Fact] @@ -476,15 +410,6 @@ public async Task DeleteAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _authorRepositoryMock.Verify(repo => repo.Delete(author), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(author.Id), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains($"Unexpected error occurred while deleting author {author.Id}.")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Unexpected error occurred while deleting author"); } } \ No newline at end of file diff --git a/tests/Library.API.Tests/Services/BookOrderServiceTests.cs b/tests/Library.API.Tests/Services/BookOrderServiceTests.cs index f002c5f..34ce88c 100644 --- a/tests/Library.API.Tests/Services/BookOrderServiceTests.cs +++ b/tests/Library.API.Tests/Services/BookOrderServiceTests.cs @@ -1,8 +1,10 @@ using System.Diagnostics; using AutoMapper; - using FluentAssertions; +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Moq; using Library.API.Domain.Models; using Library.API.Domain.Repositories; @@ -11,20 +13,13 @@ using Library.API.Tests.Helpers; using Library.Events.Messages; -using MassTransit; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using Moq; - namespace Library.API.Tests.Services; -public class BookOrderServiceTests +public class BookOrderServiceTests : ServiceTestBase { private readonly Mock _bookOrderRepositoryMock; + private readonly Mock _bookRepositoryMock; private readonly Mock _unitOfWorkMock; - private readonly Mock> _loggerMock; private readonly ActivitySource _activitySource; private readonly Mock _mapperMock; private readonly Mock _publishEndpointMock; @@ -32,8 +27,8 @@ public class BookOrderServiceTests public BookOrderServiceTests() { _bookOrderRepositoryMock = new Mock(); + _bookRepositoryMock = new Mock(); _unitOfWorkMock = new Mock(); - _loggerMock = new Mock>(); _activitySource = new ActivitySource("Library.API.Tests"); _mapperMock = new Mock(); _publishEndpointMock = new Mock(); @@ -42,8 +37,9 @@ public BookOrderServiceTests() private BookOrderService CreateService() => new BookOrderService( _bookOrderRepositoryMock.Object, + _bookRepositoryMock.Object, _unitOfWorkMock.Object, - _loggerMock.Object, + LoggerMock.Object, _activitySource, _mapperMock.Object, _publishEndpointMock.Object); @@ -131,16 +127,7 @@ public async Task FindByIdAsync_ShouldReturnError_WhenRepositoryFails() result.Message.Should().Be("An error occurred while retrieving the book order"); _bookOrderRepositoryMock.Verify(repo => repo.FindByIdAsync(bookOrderId), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while finding book order")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while finding book order"); } [Fact] @@ -150,9 +137,12 @@ public async Task AddAsync_ShouldReturnBookOrder_WhenSuccessful() var bookOrderService = CreateService(); var bookOrder = TestDataHelper.BookOrders[0]; var bookOrderId = bookOrder.Id; + var book = bookOrder.Items[0].Book; + var bookId = book.Id; _bookOrderRepositoryMock.Setup(repo => repo.AddAsync(bookOrder)).Returns(Task.CompletedTask); _bookOrderRepositoryMock.Setup(repo => repo.FindByIdAsync(bookOrderId)).ReturnsAsync(bookOrder); + _bookRepositoryMock.Setup(repo => repo.FindByIdAsync(bookId)).ReturnsAsync(book); _unitOfWorkMock.Setup(uow => uow.CompleteAsync()).Returns(Task.CompletedTask); _publishEndpointMock.Setup(publishEndpoint => publishEndpoint.Publish(It.IsAny(), CancellationToken.None)).Returns(Task.CompletedTask); @@ -179,8 +169,11 @@ public async Task AddAsync_ShouldReturnError_WhenRepositoryFails() var bookOrderService = CreateService(); var bookOrder = TestDataHelper.BookOrders[0]; var bookOrderId = bookOrder.Id; + var book = bookOrder.Items[0].Book; + var bookId = book.Id; _bookOrderRepositoryMock.Setup(repo => repo.AddAsync(bookOrder)).ThrowsAsync(new DbUpdateException("Repository error")); + _bookRepositoryMock.Setup(repo => repo.FindByIdAsync(bookId)).ReturnsAsync(book); // Act var result = await bookOrderService.AddAsync(bookOrder); @@ -197,16 +190,7 @@ public async Task AddAsync_ShouldReturnError_WhenRepositoryFails() _bookOrderRepositoryMock.Verify(repo => repo.AddAsync(bookOrder), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); _publishEndpointMock.Verify(publishEndpoint => publishEndpoint.Publish(It.IsAny(), CancellationToken.None), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while saving book order.")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while saving book order"); } [Fact] @@ -216,9 +200,12 @@ public async Task AddAsync_ShouldReturnError_WhenUnitOfWorkFails() var bookOrderService = CreateService(); var bookOrder = TestDataHelper.BookOrders[0]; var bookOrderId = bookOrder.Id; + var book = bookOrder.Items[0].Book; + var bookId = book.Id; _bookOrderRepositoryMock.Setup(repo => repo.AddAsync(bookOrder)).Returns(Task.CompletedTask); _bookOrderRepositoryMock.Setup(repo => repo.FindByIdAsync(bookOrderId)).ReturnsAsync(bookOrder); + _bookRepositoryMock.Setup(repo => repo.FindByIdAsync(bookId)).ReturnsAsync(book); _unitOfWorkMock.Setup(uow => uow.CompleteAsync()).ThrowsAsync(new DbUpdateException("Unit of work error")); // Act @@ -245,9 +232,12 @@ public async Task AddAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() var bookOrderService = CreateService(); var bookOrder = TestDataHelper.BookOrders[0]; var bookOrderId = bookOrder.Id; + var book = bookOrder.Items[0].Book; + var bookId = book.Id; _bookOrderRepositoryMock.Setup(repo => repo.AddAsync(bookOrder)).Returns(Task.CompletedTask); _bookOrderRepositoryMock.Setup(repo => repo.FindByIdAsync(bookOrderId)).ReturnsAsync(bookOrder); + _bookRepositoryMock.Setup(repo => repo.FindByIdAsync(bookId)).ReturnsAsync(book); _unitOfWorkMock.Setup(uow => uow.CompleteAsync()).ThrowsAsync(new Exception("Unexpected error")); // Act @@ -299,9 +289,12 @@ public async Task AddAsync_ShouldReturnError_WhenPublishFails() var bookOrderService = CreateService(); var bookOrder = TestDataHelper.BookOrders[0]; var bookOrderId = bookOrder.Id; + var book = bookOrder.Items[0].Book; + var bookId = book.Id; _bookOrderRepositoryMock.Setup(repo => repo.AddAsync(bookOrder)).Returns(Task.CompletedTask); _bookOrderRepositoryMock.Setup(repo => repo.FindByIdAsync(bookOrderId)).ReturnsAsync(bookOrder); + _bookRepositoryMock.Setup(repo => repo.FindByIdAsync(bookId)).ReturnsAsync(book); _unitOfWorkMock.Setup(uow => uow.CompleteAsync()).Returns(Task.CompletedTask); _publishEndpointMock .Setup(publishEndpoint => publishEndpoint.Publish(It.IsAny(), CancellationToken.None)) @@ -323,16 +316,7 @@ public async Task AddAsync_ShouldReturnError_WhenPublishFails() _bookOrderRepositoryMock.Verify(repo => repo.FindByIdAsync(bookOrderId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Once); _publishEndpointMock.Verify(publishEndpoint => publishEndpoint.Publish(It.IsAny(), CancellationToken.None), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while publishing order placed event")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while publishing order placed event"); } [Fact] @@ -414,15 +398,6 @@ public async Task UpdateStatusAsync_ShouldReturnError_WhenRepositoryFails() _bookOrderRepositoryMock.Verify(repo => repo.FindByIdAsync(bookOrderId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while updating status for book order")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while updating status for book order"); } } diff --git a/tests/Library.API.Tests/Services/BookServiceTests.cs b/tests/Library.API.Tests/Services/BookServiceTests.cs index 8823c0d..03b4412 100644 --- a/tests/Library.API.Tests/Services/BookServiceTests.cs +++ b/tests/Library.API.Tests/Services/BookServiceTests.cs @@ -13,12 +13,11 @@ namespace Library.API.Tests.Services; -public class BookServiceTests +public class BookServiceTests : ServiceTestBase { private readonly Mock _bookRepositoryMock; private readonly Mock _authorRepositoryMock; private readonly Mock _unitOfWorkMock; - private readonly Mock> _loggerMock; private readonly ActivitySource _activitySource; public BookServiceTests() @@ -26,12 +25,11 @@ public BookServiceTests() _bookRepositoryMock = new Mock(); _authorRepositoryMock = new Mock(); _unitOfWorkMock = new Mock(); - _loggerMock = new Mock>(); _activitySource = new ActivitySource("Library.API.Tests"); } private BookService CreateService() => - new BookService(_bookRepositoryMock.Object, _authorRepositoryMock.Object, _unitOfWorkMock.Object, _loggerMock.Object, _activitySource); + new BookService(_bookRepositoryMock.Object, _authorRepositoryMock.Object, _unitOfWorkMock.Object, LoggerMock.Object, _activitySource); [Fact] public async Task ListAsync_ShouldReturnBooks_WhenSuccessful() @@ -74,16 +72,7 @@ public async Task ListAsync_ShouldReturnError_WhenRepositoryFails() result.Message.Should().Be("An error occurred while retrieving the books"); _bookRepositoryMock.Verify(repo => repo.ListAsync(), Times.Once); - _loggerMock.Verify( - static x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while listing books")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while listing books"); } [Fact] @@ -150,16 +139,7 @@ public async Task FindByIdAsync_ShouldReturnError_WhenRepositoryFails() result.Message.Should().Be("An error occurred while retrieving the book"); _bookRepositoryMock.Verify(repo => repo.FindByIdAsync(book.Id), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while finding book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while finding book"); } [Fact] @@ -213,16 +193,7 @@ public async Task AddAsync_ShouldReturnError_WhenRepositoryFails() _bookRepositoryMock.Verify(repo => repo.AddAsync(book), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(book.AuthorId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while saving book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while saving book"); } [Fact] @@ -250,16 +221,7 @@ public async Task AddAsync_ShouldReturnError_WhenUnitOfWorkFails() _bookRepositoryMock.Verify(repo => repo.AddAsync(book), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(book.AuthorId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Once); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while saving book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while saving book"); } [Fact] @@ -286,16 +248,7 @@ public async Task AddAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _bookRepositoryMock.Verify(repo => repo.AddAsync(book), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(book.AuthorId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Unexpected error occurred while adding book.")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Unexpected error occurred while adding book."); } [Fact] @@ -426,16 +379,7 @@ public async Task UpdateAsync_ShouldReturnError_WhenRepositoryFails() _bookRepositoryMock.Verify(repo => repo.Update(book), Times.Once); _authorRepositoryMock.Verify(repo => repo.FindByIdAsync(book.AuthorId), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while updating book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while updating book"); } [Fact] @@ -463,16 +407,7 @@ public async Task UpdateAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _bookRepositoryMock.Verify(repo => repo.FindByIdAsync(book.Id), Times.Once); _bookRepositoryMock.Verify(repo => repo.Update(book), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Unexpected error occurred while updating book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Unexpected error occurred while updating book"); } [Fact] @@ -548,16 +483,7 @@ public async Task DeleteAsync_ShouldReturnError_WhenRepositoryFails() _bookRepositoryMock.Verify(repo => repo.FindByIdAsync(book.Id), Times.Once); _bookRepositoryMock.Verify(repo => repo.Delete(book), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Error occurred while deleting book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Error occurred while deleting book"); } [Fact] @@ -583,15 +509,6 @@ public async Task DeleteAsync_ShouldReturnError_WhenUnexpectedErrorOccurs() _bookRepositoryMock.Verify(repo => repo.FindByIdAsync(book.Id), Times.Once); _bookRepositoryMock.Verify(repo => repo.Delete(book), Times.Once); _unitOfWorkMock.Verify(uow => uow.CompleteAsync(), Times.Never); - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v!.ToString()!.Contains("Unexpected error occurred while deleting book")), - It.IsAny(), - It.IsAny>() - ), - Times.Once - ); + VerifyErrorLog("Unexpected error occurred while deleting book"); } } \ No newline at end of file diff --git a/tests/Library.API.Tests/Services/ServiceTestBase.cs b/tests/Library.API.Tests/Services/ServiceTestBase.cs new file mode 100644 index 0000000..d69e0b9 --- /dev/null +++ b/tests/Library.API.Tests/Services/ServiceTestBase.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; + +using Moq; + +namespace Library.API.Tests.Services; + +public class ServiceTestBase +{ + protected readonly Mock> LoggerMock = new(); + + protected void VerifyErrorLog(string expectedMessage) + { + LoggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v!.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } +}