From 182e12714dfff22eef6f5350f98231ba6523e0de Mon Sep 17 00:00:00 2001 From: Mezdelex Date: Tue, 12 Nov 2024 20:55:56 +0100 Subject: [PATCH] feat(unittests): added all the remaining tests for Expenses and pending GetAllCategoriesQuery tests; used MockQueryable.Moq library to handle IAsyncQueriableProvider implementation --- src/Domain/Entities/Category.cs | 4 +- src/Domain/Entities/Expense.cs | 8 +- .../Extensions/InfrastructureExtension.cs | 6 +- src/Infrastructure/Infrastructure.csproj | 3 +- src/WebApi/WebApi.csproj | 2 +- .../DeleteCategoryCommandHandlerTests.cs | 7 +- .../DeleteExpenseCommandHandlerTests.cs | 39 ++++++++ .../PatchCategoryCommandHandlerTests.cs | 19 +++- .../PatchExpenseCommandHandlerTests.cs | 69 +++++++++++++ .../PostCategoryCommandHandlerTests.cs | 17 +++- .../PostExpenseCommandHandlerTests.cs | 68 +++++++++++++ .../GetAllCategoriesQueryHandlerTests.cs | 17 +--- .../GetAllExpensesQueryHandlerTests.cs | 99 +++++++++++++++++++ .../Queries/GetExpenseQueryHandlerTests.cs | 50 ++++++++++ tests/UnitTests/GlobalUsings.cs | 9 +- tests/UnitTests/UnitTests.csproj | 1 + 16 files changed, 381 insertions(+), 37 deletions(-) create mode 100644 tests/UnitTests/Features/Commands/DeleteExpenseCommandHandlerTests.cs create mode 100644 tests/UnitTests/Features/Commands/PatchExpenseCommandHandlerTests.cs create mode 100644 tests/UnitTests/Features/Commands/PostExpenseCommandHandlerTests.cs create mode 100644 tests/UnitTests/Features/Queries/GetAllExpensesQueryHandlerTests.cs create mode 100644 tests/UnitTests/Features/Queries/GetExpenseQueryHandlerTests.cs diff --git a/src/Domain/Entities/Category.cs b/src/Domain/Entities/Category.cs index c82bcfe..263238b 100644 --- a/src/Domain/Entities/Category.cs +++ b/src/Domain/Entities/Category.cs @@ -2,8 +2,8 @@ namespace Domain.Entities; public class Category : BaseEntity { - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public virtual string Name { get; set; } = string.Empty; + public virtual string Description { get; set; } = string.Empty; public virtual List Expenses { get; set; } = default!; } diff --git a/src/Domain/Entities/Expense.cs b/src/Domain/Entities/Expense.cs index 362ad1e..9d53981 100644 --- a/src/Domain/Entities/Expense.cs +++ b/src/Domain/Entities/Expense.cs @@ -2,10 +2,10 @@ namespace Domain.Entities; public class Expense : BaseEntity { - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public double Value { get; set; } - public Guid CategoryId { get; set; } + public virtual string Name { get; set; } = string.Empty; + public virtual string Description { get; set; } = string.Empty; + public virtual double Value { get; set; } + public virtual Guid CategoryId { get; set; } public virtual Category Category { get; set; } = default!; } diff --git a/src/Infrastructure/Extensions/InfrastructureExtension.cs b/src/Infrastructure/Extensions/InfrastructureExtension.cs index aa2235f..16cf521 100644 --- a/src/Infrastructure/Extensions/InfrastructureExtension.cs +++ b/src/Infrastructure/Extensions/InfrastructureExtension.cs @@ -8,10 +8,12 @@ IConfiguration configuration ) { services.AddDbContext(options => + { + options.UseLazyLoadingProxies(); options.UseSqlServer( $"Server=sqlserver;Database={configuration["DATABASE"]};User Id=sa;Password={configuration["PASSWORD"]};TrustServerCertificate=True" - ) - ); + ); + }); services.AddScoped(provider => provider.GetRequiredService() ); diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 7cb2bdd..2193d73 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -11,7 +11,8 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index 7af33f0..566eb85 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs index c9472e0..04c46cb 100644 --- a/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs @@ -30,7 +30,10 @@ public async Task DeleteCategoryCommandHandler_ShouldDeleteCategory() await _handler.Handle(deleteCategoryCommand, _cancellationToken); // Assert - _repository.Verify(); - _uow.Verify(); + _repository.Verify( + mock => mock.DeleteAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); } } diff --git a/tests/UnitTests/Features/Commands/DeleteExpenseCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/DeleteExpenseCommandHandlerTests.cs new file mode 100644 index 0000000..8d6e461 --- /dev/null +++ b/tests/UnitTests/Features/Commands/DeleteExpenseCommandHandlerTests.cs @@ -0,0 +1,39 @@ +namespace Application.UnitTests.Expenses.PostAsync; + +public sealed class DeleteExpenseCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken; + private readonly Mock _repository; + private readonly Mock _uow; + private readonly DeleteExpenseCommandHandler _handler; + + public DeleteExpenseCommandHandlerTests() + { + _cancellationToken = new(); + _repository = new(); + _uow = new(); + + _handler = new DeleteExpenseCommandHandler(_repository.Object, _uow.Object); + } + + [Fact] + public async Task DeleteExpenseCommandHandler_ShouldDeleteExpense() + { + // Arrange + var deleteExpenseCommand = new DeleteExpenseCommand(Guid.NewGuid()); + _repository + .Setup(mock => mock.DeleteAsync(It.IsAny(), _cancellationToken)) + .Verifiable(); + _uow.Setup(mock => mock.SaveChangesAsync(_cancellationToken)).Verifiable(); + + // Act + await _handler.Handle(deleteExpenseCommand, _cancellationToken); + + // Assert + _repository.Verify( + mock => mock.DeleteAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); + } +} diff --git a/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs index ac73f21..9c655bc 100644 --- a/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs @@ -35,7 +35,7 @@ public async Task PatchCategoryCommandHandler_ShouldPatchCategoryAndPublishEvent "Category 1 description" ); _validator - .Setup(mock => mock.ValidateAsync(patchCategoryCommand, _cancellationToken)) + .Setup(mock => mock.ValidateAsync(It.IsAny(), _cancellationToken)) .ReturnsAsync(new ValidationResult()) .Verifiable(); _repository @@ -50,9 +50,18 @@ public async Task PatchCategoryCommandHandler_ShouldPatchCategoryAndPublishEvent await _handler.Handle(patchCategoryCommand, _cancellationToken); // Assert - _validator.Verify(); - _repository.Verify(); - _uow.Verify(); - _eventBus.Verify(); + _validator.Verify( + mock => mock.ValidateAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _repository.Verify( + mock => mock.PatchAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); + _eventBus.Verify( + mock => mock.PublishAsync(It.IsAny(), _cancellationToken), + Times.Once + ); } } diff --git a/tests/UnitTests/Features/Commands/PatchExpenseCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PatchExpenseCommandHandlerTests.cs new file mode 100644 index 0000000..c89181c --- /dev/null +++ b/tests/UnitTests/Features/Commands/PatchExpenseCommandHandlerTests.cs @@ -0,0 +1,69 @@ +namespace Application.UnitTests.Expenses.PostAsync; + +public sealed class PatchExpenseCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken; + private readonly Mock> _validator; + private readonly Mock _repository; + private readonly Mock _uow; + private readonly Mock _eventBus; + private readonly PatchExpenseCommandHandler _handler; + + public PatchExpenseCommandHandlerTests() + { + _cancellationToken = new(); + _validator = new(); + _repository = new(); + _uow = new(); + _eventBus = new(); + + _handler = new PatchExpenseCommandHandler( + _validator.Object, + _repository.Object, + _uow.Object, + _eventBus.Object + ); + } + + [Fact] + public async Task PatchExpenseCommandHandler_ShouldPatchExpenseAndPublishEventAsync() + { + // Arrange + var patchExpenseCommand = new PatchExpenseCommand( + Guid.NewGuid(), + "Expense 1 name", + "Expense 1 description", + 1, + new Guid() + ); + _validator + .Setup(mock => mock.ValidateAsync(It.IsAny(), _cancellationToken)) + .ReturnsAsync(new ValidationResult()) + .Verifiable(); + _repository + .Setup(mock => mock.PatchAsync(It.IsAny(), _cancellationToken)) + .Verifiable(); + _uow.Setup(mock => mock.SaveChangesAsync(_cancellationToken)).Verifiable(); + _eventBus + .Setup(mock => mock.PublishAsync(It.IsAny(), _cancellationToken)) + .Verifiable(); + + // Act + await _handler.Handle(patchExpenseCommand, _cancellationToken); + + // Assert + _validator.Verify( + mock => mock.ValidateAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _repository.Verify( + mock => mock.PatchAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); + _eventBus.Verify( + mock => mock.PublishAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + } +} diff --git a/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs index 02cda6e..9a1621e 100644 --- a/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs @@ -49,9 +49,18 @@ public async Task PostCategoryCommandHandler_ShouldPostCategoryAndPublishEventAs await _handler.Handle(postCategoryCommand, _cancellationToken); // Assert - _validator.Verify(); - _repository.Verify(); - _uow.Verify(); - _eventBus.Verify(); + _validator.Verify( + mock => mock.ValidateAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _repository.Verify( + mock => mock.PostAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); + _eventBus.Verify( + mock => mock.PublishAsync(It.IsAny(), _cancellationToken), + Times.Once + ); } } diff --git a/tests/UnitTests/Features/Commands/PostExpenseCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PostExpenseCommandHandlerTests.cs new file mode 100644 index 0000000..f311892 --- /dev/null +++ b/tests/UnitTests/Features/Commands/PostExpenseCommandHandlerTests.cs @@ -0,0 +1,68 @@ +namespace Application.UnitTests.Expenses.PostAsync; + +public sealed class PostExpenseCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken; + private readonly Mock> _validator; + private readonly Mock _repository; + private readonly Mock _uow; + private readonly Mock _eventBus; + private readonly PostExpenseCommandHandler _handler; + + public PostExpenseCommandHandlerTests() + { + _cancellationToken = new(); + _validator = new(); + _repository = new(); + _uow = new(); + _eventBus = new(); + + _handler = new PostExpenseCommandHandler( + _validator.Object, + _repository.Object, + _uow.Object, + _eventBus.Object + ); + } + + [Fact] + public async Task PostExpenseCommandHandler_ShouldPostExpenseAndPublishEventAsync() + { + // Arrange + var postExpenseCommand = new PostExpenseCommand( + "Expense 1 name", + "Expense 1 description", + 1, + new Guid() + ); + _validator + .Setup(mock => mock.ValidateAsync(postExpenseCommand, _cancellationToken)) + .ReturnsAsync(new ValidationResult()) + .Verifiable(); + _repository + .Setup(mock => mock.PostAsync(It.IsAny(), _cancellationToken)) + .Verifiable(); + _uow.Setup(mock => mock.SaveChangesAsync(_cancellationToken)).Verifiable(); + _eventBus + .Setup(mock => mock.PublishAsync(It.IsAny(), _cancellationToken)) + .Verifiable(); + + // Act + await _handler.Handle(postExpenseCommand, _cancellationToken); + + // Assert + _validator.Verify( + mock => mock.ValidateAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _repository.Verify( + mock => mock.PostAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + _uow.Verify(mock => mock.SaveChangesAsync(_cancellationToken), Times.Once); + _eventBus.Verify( + mock => mock.PublishAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + } +} diff --git a/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs b/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs index de55be4..8bf385c 100644 --- a/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs +++ b/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs @@ -16,19 +16,17 @@ public GetAllCategoriesQueryHandlerTests() _handler = new GetAllCategoriesQueryHandler(_repository.Object, _redisCache.Object); } - [Fact(Skip = "Pending IAsyncQueryProvider mock implementation")] + [Fact] public async Task GetAllCategoriesQueryHandler_ShouldReturnPagedListOfRequestedCategoriesAsListOfCategoryDTOAndMetadata() { // Arrange - var name = "Test"; - var containedWord = "es"; + var containedWord = "am"; var page = 1; var pageSize = 2; var getAllCategoriesQuery = new GetAllCategoriesQuery { Page = page, PageSize = pageSize, - Name = name, ContainedWord = containedWord, }; var redisKey = $"{nameof(GetAllCategoriesQuery)}#{page}#{pageSize}"; @@ -47,17 +45,10 @@ public async Task GetAllCategoriesQueryHandler_ShouldReturnPagedListOfRequestedC Description = "Description 2", }, }; - var pagedCategories = new PagedList( - categories, - page, - pageSize, - categories.Count, - false, - false - ); _repository .Setup(mock => mock.ApplySpecification(It.IsAny())) - .Returns(categories.AsQueryable); + .Returns(categories.BuildMock()) + .Verifiable(); _redisCache .Setup(mock => mock.GetCachedData>(redisKey)) .ReturnsAsync((PagedList)null!); diff --git a/tests/UnitTests/Features/Queries/GetAllExpensesQueryHandlerTests.cs b/tests/UnitTests/Features/Queries/GetAllExpensesQueryHandlerTests.cs new file mode 100644 index 0000000..c53a80f --- /dev/null +++ b/tests/UnitTests/Features/Queries/GetAllExpensesQueryHandlerTests.cs @@ -0,0 +1,99 @@ +namespace UnitTests.Features.Queries; + +public sealed class GetAllExpensesQueryHandlerTests +{ + private readonly CancellationToken _cancellationToken; + private readonly Mock _repository; + private readonly Mock _redisCache; + private readonly GetAllExpensesQueryHandler _handler; + + public GetAllExpensesQueryHandlerTests() + { + _cancellationToken = new(); + _repository = new(); + _redisCache = new(); + + _handler = new GetAllExpensesQueryHandler(_repository.Object, _redisCache.Object); + } + + [Fact] + public async Task GetAllExpensesQueryHandler_ShouldReturnPagedListOfRequestedExpensesAsListOfExpenseDTOAndMetadata() + { + // Arrange + var containedWord = "am"; + var page = 1; + var pageSize = 2; + var getAllExpensesQuery = new GetAllExpensesQuery + { + Page = page, + PageSize = pageSize, + ContainedWord = containedWord, + }; + var redisKey = $"{nameof(GetAllExpensesQuery)}#{page}#{pageSize}"; + var expenses = new List + { + new() + { + Id = Guid.NewGuid(), + Name = "Name 1", + Description = "Description 1", + Value = 1, + }, + new() + { + Id = Guid.NewGuid(), + Name = "Name 2", + Description = "Description 2", + Value = 2, + }, + }; + _repository + .Setup(mock => mock.ApplySpecification(It.IsAny())) + .Returns(expenses.BuildMock()) + .Verifiable(); + _redisCache + .Setup(mock => mock.GetCachedData>(redisKey)) + .ReturnsAsync((PagedList)null!); + _redisCache + .Setup(mock => + mock.SetCachedData( + redisKey, + It.IsAny>(), + It.IsAny() + ) + ) + .Returns(Task.CompletedTask) + .Verifiable(); + + // Act + var result = await _handler.Handle(getAllExpensesQuery, _cancellationToken); + + // Assert + result.Items[0].Id.Should().Be(expenses[0].Id); + result.Items[0].Name.Should().Be(expenses[0].Name); + result.Items[0].Description.Should().Be(expenses[0].Description); + result.Items[0].Value.Should().Be(expenses[0].Value); + result.Items[1].Id.Should().Be(expenses[1].Id); + result.Items[1].Name.Should().Be(expenses[1].Name); + result.Items[1].Value.Should().Be(expenses[1].Value); + result.TotalCount.Should().Be(expenses.Count); + result.Page.Should().Be(page); + result.PageSize.Should().Be(pageSize); + result.HasPreviousPage.Should().Be(false); + result.HasNextPage.Should().Be(false); + _repository.Verify( + mock => mock.ApplySpecification(It.IsAny()), + Times.Once + ); + _redisCache.Verify(mock => mock.GetCachedData>(redisKey), Times.Once); + _redisCache.Verify( + mock => + mock.SetCachedData( + redisKey, + It.IsAny>(), + It.IsAny() + ), + Times.Once + ); + } +} diff --git a/tests/UnitTests/Features/Queries/GetExpenseQueryHandlerTests.cs b/tests/UnitTests/Features/Queries/GetExpenseQueryHandlerTests.cs new file mode 100644 index 0000000..1098458 --- /dev/null +++ b/tests/UnitTests/Features/Queries/GetExpenseQueryHandlerTests.cs @@ -0,0 +1,50 @@ +namespace Application.UnitTests.Expenses.GetAsync; + +public sealed class GetExpenseQueryHandlerTests +{ + private readonly CancellationToken _cancellationToken; + private readonly Mock _repository; + private readonly GetExpenseQueryHandler _handler; + + public GetExpenseQueryHandlerTests() + { + _cancellationToken = new(); + _repository = new(); + + _handler = new GetExpenseQueryHandler(_repository.Object); + } + + [Fact] + public async Task Handle_ValidIdGetExpenseQuery_ShouldReturnRequestedExpenseAsExpenseDTOAsync() + { + // Arrange + var guid = Guid.NewGuid(); + var getExpenseQuery = new GetExpenseQuery(guid); + var category = new Expense + { + Id = guid, + Name = "Name 1", + Description = "Description 1", + CategoryId = guid, + }; + _repository + .Setup(mock => + mock.GetBySpecAsync(It.IsAny(), _cancellationToken) + ) + .ReturnsAsync(category) + .Verifiable(); + + // Act + var result = await _handler.Handle(getExpenseQuery, _cancellationToken); + + // Assert + result.Id.Should().Be(category.Id); + result.Name.Should().Be(category.Name); + result.Description.Should().Be(category.Description); + result.CategoryId.Should().Be(category.CategoryId); + _repository.Verify( + mock => mock.GetBySpecAsync(It.IsAny(), _cancellationToken), + Times.Once + ); + } +} diff --git a/tests/UnitTests/GlobalUsings.cs b/tests/UnitTests/GlobalUsings.cs index 6360ebe..bc189c1 100644 --- a/tests/UnitTests/GlobalUsings.cs +++ b/tests/UnitTests/GlobalUsings.cs @@ -1,14 +1,18 @@ global using System.Linq.Expressions; global using Application.Abstractions; -global using Application.Contexts; global using Application.Features.Commands; global using static Application.Features.Commands.DeleteCategoryCommand; +global using static Application.Features.Commands.DeleteExpenseCommand; global using static Application.Features.Commands.PatchCategoryCommand; +global using static Application.Features.Commands.PatchExpenseCommand; global using static Application.Features.Commands.PostCategoryCommand; +global using static Application.Features.Commands.PostExpenseCommand; global using Application.Features.DomainEvents; global using Application.Features.Queries; global using static Application.Features.Queries.GetAllCategoriesQuery; +global using static Application.Features.Queries.GetAllExpensesQuery; global using static Application.Features.Queries.GetCategoryQuery; +global using static Application.Features.Queries.GetExpenseQuery; global using Application.Features.Shared; global using Application.Repositories; global using Domain.Cache; @@ -19,7 +23,6 @@ global using FluentAssertions; global using FluentValidation; global using FluentValidation.Results; -global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Query; +global using MockQueryable; global using Moq; -global using UnitTests.Shared; diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index 35c3b20..c592f94 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -13,6 +13,7 @@ +