0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-29 18:21:08 +00:00

Add integration tests

This commit is contained in:
Alexander Konietzko 2023-03-09 16:48:04 +01:00
parent e937e786a7
commit bbf2ce4ca2
No known key found for this signature in database
GPG Key ID: BA6905F37AEC2B5B
21 changed files with 513 additions and 27 deletions

View File

@ -38,8 +38,8 @@ public class UserController : ApiController
[HttpPost]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
{
await _userService.CreateUserAsync(viewModel);
return Response();
var userId = await _userService.CreateUserAsync(viewModel);
return Response(userId);
}
[HttpDelete("{id}")]

View File

@ -16,12 +16,10 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
{
UserRepository = new();
SetupUserRepositoryGetAllAsync();
Handler = new(UserRepository.Object);
}
private void SetupUserRepositoryGetAllAsync()
public void SetupUserAsync()
{
var user = new Mock<User>(() =>
new User(

View File

@ -16,12 +16,10 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
{
UserRepository = new();
SetupUserRepositoryGetAllAsync();
Handler = new(UserRepository.Object, Bus.Object);
}
private void SetupUserRepositoryGetAllAsync()
public void SetupUserAsync()
{
var user = new Mock<User>(() =>
new User(

View File

@ -11,6 +11,8 @@ public sealed class GetAllUsersQueryHandlerTests
[Fact]
public async Task Should_Get_All_Users()
{
_fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle(
new(),
default);
@ -21,4 +23,6 @@ public sealed class GetAllUsersQueryHandlerTests
result.Should().ContainSingle();
result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId);
}
// Add Test for deleted user
}

View File

@ -13,6 +13,8 @@ public sealed class GetUserByIdQueryHandlerTests
[Fact]
public async Task Should_Get_Existing_User()
{
_fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle(
new(_fixture.ExistingUserId),
default);
@ -26,6 +28,8 @@ public sealed class GetUserByIdQueryHandlerTests
[Fact]
public async Task Should_Raise_Notification_For_No_User()
{
_fixture.SetupUserAsync();
var request = new GetUserByIdQuery(Guid.NewGuid());
var result = await _fixture.Handler.Handle(
request,
@ -38,4 +42,6 @@ public sealed class GetUserByIdQueryHandlerTests
result.Should().BeNull();
}
// Add Test for deleted user
}

View File

@ -9,7 +9,7 @@ public interface IUserService
{
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId);
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
public Task CreateUserAsync(CreateUserViewModel user);
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);
public Task DeleteUserAsync(Guid userId);
}

View File

@ -24,6 +24,7 @@ public sealed class GetAllUsersQueryHandler :
{
return await _userRepository
.GetAllNoTracking()
.Where(x => !x.Deleted)
.Select(x => UserViewModel.FromUser(x))
.ToListAsync(cancellationToken);
}

View File

@ -27,7 +27,7 @@ public sealed class GetUserByIdQueryHandler :
{
var user = _userRepository
.GetAllNoTracking()
.FirstOrDefault(x => x.Id == request.UserId);
.FirstOrDefault(x => x.Id == request.UserId && !x.Deleted);
if (user == null)
{

View File

@ -31,13 +31,17 @@ public sealed class UserService : IUserService
return await _bus.QueryAsync(new GetAllUsersQuery());
}
public async Task CreateUserAsync(CreateUserViewModel user)
public async Task<Guid> CreateUserAsync(CreateUserViewModel user)
{
var userId = Guid.NewGuid();
await _bus.SendCommandAsync(new CreateUserCommand(
Guid.NewGuid(),
userId,
user.Email,
user.Surname,
user.GivenName));
return userId;
}
public async Task UpdateUserAsync(UpdateUserViewModel user)

View File

@ -14,8 +14,8 @@ public class User : Entity
public User(
Guid id,
string email,
string givenName,
string surname) : base(id)
string surname,
string givenName) : base(id)
{
Email = email;
GivenName = givenName;

View File

@ -0,0 +1,63 @@
using CleanArchitecture.Domain.Notifications;
using FluentAssertions;
using Xunit;
namespace CleanArchitecture.Infrastructure.Tests;
public sealed class DomainNotificationHandlerTests
{
[Fact]
public void Should_Create_DomainNotificationHandler_Instance()
{
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.GetNotifications().Should().BeEmpty();
}
[Fact]
public void Should_Handle_DomainNotification()
{
string key = "Key";
string value = "Value";
string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.Handle(domainNotification);
domainNotificationHandler.GetNotifications().Should().HaveCount(1);
}
[Fact]
public void Should_Handle_DomainNotification_Overload()
{
string key = "Key";
string value = "Value";
string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.Handle(domainNotification, default);
domainNotificationHandler.GetNotifications().Should().HaveCount(1);
}
[Fact]
public void DomainNotification_HasNotifications_After_Handling_One()
{
string key = "Key";
string value = "Value";
string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.Handle(domainNotification);
domainNotificationHandler.HasNotifications().Should().BeTrue();
}
[Fact]
public void DomainNotification_HasNotifications_False_Not_Handling_One()
{
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.HasNotifications().Should().BeFalse();
}
}

View File

@ -0,0 +1,40 @@
using CleanArchitecture.Domain.Notifications;
using FluentAssertions;
using Xunit;
namespace CleanArchitecture.Infrastructure.Tests;
public sealed class DomainNotificationTests
{
[Fact]
public void Should_Create_DomainNotification_Instance()
{
string key = "Key";
string value = "Value";
string code = "Code";
var domainNotification = new DomainNotification(
key, value, code);
domainNotification.Key.Should().Be(key);
domainNotification.Value.Should().Be(value);
domainNotification.Should().NotBe(default(Guid));
domainNotification.Code.Should().Be(code);
}
[Fact]
public void Should_Create_DomainNotification_Overload_Instance()
{
string key = "Key";
string value = "Value";
string code = "Code";
var domainNotification = new DomainNotification(
key, value, code);
domainNotification.Key.Should().Be(key);
domainNotification.Value.Should().Be(value);
domainNotification.Code.Should().Be(code);
domainNotification.Should().NotBe(default(Guid));
}
}

View File

@ -9,16 +9,31 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="UtilityTests\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,164 @@
using System.Net;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures;
using FluentAssertions;
using Xunit;
using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.Controller;
[Collection("Integrationtests")]
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class UserControllerTests : IClassFixture<UserTestFixture>
{
private readonly UserTestFixture _fixture;
public UserControllerTests(UserTestFixture fixture)
{
_fixture = fixture;
}
[Fact, Priority(0)]
public async Task Should_Get_No_User()
{
var response = await _fixture.ServerClient.GetAsync("user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Should().BeNullOrEmpty();
}
[Fact, Priority(5)]
public async Task Should_Create_User()
{
var user = new CreateUserViewModel("test@email.com", "Test", "Email");
var response = await _fixture.ServerClient.PostAsJsonAsync("user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<Guid>();
message?.Data.Should().NotBeEmpty();
_fixture.CreatedUserId = message!.Data;
}
[Fact, Priority(10)]
public async Task Should_Get_Created_Users()
{
var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<UserViewModel>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Id.Should().Be(_fixture.CreatedUserId);
content.Email.Should().Be("test@email.com");
content.Surname.Should().Be("Test");
content.GivenName.Should().Be("Email");
}
[Fact, Priority(15)]
public async Task Should_Update_User()
{
var user = new UpdateUserViewModel(
_fixture.CreatedUserId,
"newtest@email.com",
"NewTest",
"NewEmail");
var response = await _fixture.ServerClient.PutAsJsonAsync("user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<UpdateUserViewModel>();
message?.Data.Should().NotBeNull();
var content = message!.Data;
content.Should().BeEquivalentTo(user);
}
[Fact, Priority(20)]
public async Task Should_Get_Updated_Users()
{
var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<UserViewModel>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Id.Should().Be(_fixture.CreatedUserId);
content.Email.Should().Be("newtest@email.com");
content.Surname.Should().Be("NewTest");
content.GivenName.Should().Be("NewEmail");
}
[Fact, Priority(25)]
public async Task Should_Get_One_User()
{
var response = await _fixture.ServerClient.GetAsync("user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Should().ContainSingle();
content.First().Id.Should().Be(_fixture.CreatedUserId);
content.First().Email.Should().Be("newtest@email.com");
content.First().Surname.Should().Be("NewTest");
content.First().GivenName.Should().Be("NewEmail");
}
[Fact, Priority(30)]
public async Task Should_Delete_User()
{
var response = await _fixture.ServerClient.DeleteAsync("user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<Guid>();
message?.Data.Should().NotBeEmpty();
var content = message!.Data!;
content.Should().Be(_fixture.CreatedUserId);
}
[Fact, Priority(35)]
public async Task Should_Get_No_User_Again()
{
var response = await _fixture.ServerClient.GetAsync("user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Should().BeNullOrEmpty();
}
}

View File

@ -0,0 +1,42 @@
using System.Data.Common;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace CleanArchitecture.IntegrationTests.Extensions;
public static class FunctionalTestsServiceCollectionExtensions
{
public static IServiceCollection SetupTestDatabase<TContext>(this IServiceCollection services, DbConnection connection) where TContext : DbContext
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddScoped(p =>
DbContextOptionsFactory<TContext>(
p,
(sp, options) => options
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
.UseLazyLoadingProxies()
.UseSqlite(connection)));
return services;
}
private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(
IServiceProvider applicationServiceProvider,
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction)
where TContext : DbContext
{
var builder = new DbContextOptionsBuilder<TContext>(
new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>()));
builder.UseApplicationServiceProvider(applicationServiceProvider);
optionsAction?.Invoke(applicationServiceProvider, builder);
return builder.Options;
}
}

View File

@ -0,0 +1,56 @@
using System.Text;
using System.Text.Json;
using CleanArchitecture.Api.Models;
namespace CleanArchitecture.IntegrationTests.Extensions;
public static class HttpExensions
{
public static JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
};
private static T? Deserialize<T>(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return default;
}
return JsonSerializer.Deserialize<T>(json, JsonSerializerOptions);
}
private static string Serialize<T>(T data)
{
return JsonSerializer.Serialize(data, JsonSerializerOptions);
}
public static async Task<ResponseMessage<T>?> ReadAsJsonAsync<T>(this HttpContent httpContent)
{
var stringContent = await httpContent.ReadAsStringAsync();
return Deserialize<ResponseMessage<T>>(stringContent);
}
public static Task<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient httpClient, string url, T data)
{
var content = new StringContent(Serialize(data), Encoding.UTF8, "application/json");
return httpClient.PatchAsync(url, content);
}
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(this HttpClient httpClient, string url, T data)
{
var content = new StringContent(Serialize(data), Encoding.UTF8, "application/json");
return httpClient.PostAsync(url, content);
}
public static Task<HttpResponseMessage> PutAsJsonAsync<T>(this HttpClient httpClient, string url, T data)
{
var content = new StringContent(Serialize(data), Encoding.UTF8, "application/json");
return httpClient.PutAsync(url, content);
}
}

View File

@ -0,0 +1,35 @@
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.IntegrationTests.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Fixtures;
public class TestFixtureBase
{
public HttpClient ServerClient { get; }
public TestFixtureBase()
{
var projectDir = Directory.GetCurrentDirectory();
var configPath = Path.Combine(projectDir, "appsettings.Integration.json");
var factory = new CleanArchitectureWebApplicationFactory(
SeedTestData,
RegisterCustomServicesHandler,
configPath);
ServerClient = factory.CreateClient();
ServerClient.Timeout = TimeSpan.FromMinutes(5);
}
protected virtual void SeedTestData(ApplicationDbContext context)
{
}
protected virtual void RegisterCustomServicesHandler(
IServiceCollection services,
ServiceProvider serviceProvider,
IServiceProvider scopedServices)
{
}
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.IntegrationTests.Fixtures;
public sealed class UserTestFixture : TestFixtureBase
{
public Guid CreatedUserId { get; set; }
}

View File

@ -0,0 +1,64 @@
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions;
using CleanArchitecture.IntegrationTests.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Infrastructure;
public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFactory<Program>
{
public delegate void AddCustomSeedDataHandler(ApplicationDbContext context);
public delegate void RegisterCustomServicesHandler(
IServiceCollection services,
ServiceProvider serviceProvider,
IServiceProvider scopedServices);
private readonly SqliteConnection _connection = new($"DataSource=:memory:");
private readonly AddCustomSeedDataHandler? _addCustomSeedDataHandler;
private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler;
private readonly string? _environment;
public CleanArchitectureWebApplicationFactory(
AddCustomSeedDataHandler? addCustomSeedDataHandler,
RegisterCustomServicesHandler? registerCustomServicesHandler,
string? environment = null)
{
_addCustomSeedDataHandler = addCustomSeedDataHandler;
_registerCustomServicesHandler = registerCustomServicesHandler;
_environment = environment;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
if (!string.IsNullOrWhiteSpace(_environment))
{
builder.UseEnvironment(_environment);
}
base.ConfigureWebHost(builder);
_connection.Open();
builder.ConfigureServices(services =>
{
services.SetupTestDatabase<ApplicationDbContext>(_connection);
var sp = services.BuildServiceProvider();
using IServiceScope scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var applicationDbContext = scopedServices.GetRequiredService<ApplicationDbContext>();
applicationDbContext.EnsureMigrationsApplied();
_addCustomSeedDataHandler?.Invoke(applicationDbContext);
_registerCustomServicesHandler?.Invoke(services, sp, scopedServices);
});
}
}

View File

@ -1,9 +0,0 @@
namespace CleanArchitecture.IntegrationTests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -1 +0,0 @@
global using Xunit;