Skip to content

Commit

Permalink
feat: add api integration test with testcontainer
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom Brereton authored and Tom Brereton committed Apr 18, 2024
1 parent 5282128 commit 239cc19
Show file tree
Hide file tree
Showing 21 changed files with 176 additions and 117 deletions.
7 changes: 7 additions & 0 deletions Appointer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTests", "tests\Api.IntegrationTests\Api.IntegrationTests.csproj", "{1F302DCD-2100-42CA-850A-570C30230FDF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{E9158132-1CA0-4B10-B1FB-ACA60605246B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -40,11 +42,16 @@ Global
{1F302DCD-2100-42CA-850A-570C30230FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F302DCD-2100-42CA-850A-570C30230FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F302DCD-2100-42CA-850A-570C30230FDF}.Release|Any CPU.Build.0 = Release|Any CPU
{E9158132-1CA0-4B10-B1FB-ACA60605246B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9158132-1CA0-4B10-B1FB-ACA60605246B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9158132-1CA0-4B10-B1FB-ACA60605246B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9158132-1CA0-4B10-B1FB-ACA60605246B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{DC36BD6E-301B-4C03-970E-6869B7907B0F} = {0822B695-D2DF-4E72-BCAD-DD767655E9C4}
{9514D439-B4C7-42F2-B9B9-7373C1BAAE0E} = {5E191623-BFC0-4FEC-93A9-047999771EFE}
{F7E76506-B0E8-4277-9EF1-958FA786F3D5} = {0822B695-D2DF-4E72-BCAD-DD767655E9C4}
{1F302DCD-2100-42CA-850A-570C30230FDF} = {5E191623-BFC0-4FEC-93A9-047999771EFE}
{E9158132-1CA0-4B10-B1FB-ACA60605246B} = {0822B695-D2DF-4E72-BCAD-DD767655E9C4}
EndGlobalSection
EndGlobal
4 changes: 4 additions & 0 deletions src/Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
</Content>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>

</Project>
13 changes: 11 additions & 2 deletions src/Api/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
using Appointer.Api.Requests;
using Appointer.Api.Responses;
using Appointer.Domain.Accounts;
using Appointer.Infrastructure.DbContext;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Appointer.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
public AccountController()
private readonly AppointerDbContext _dbContext;

public AccountController(AppointerDbContext dbContext)
{
_dbContext = dbContext;
}

[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateAccountRequest request,
CancellationToken cancellationToken)
{
var response = new CreateAccountResponse(Guid.NewGuid(), request.FullName);
var userAccount = new UserAccount(Guid.NewGuid(), request.FullName);
await _dbContext.UserAccounts.AddAsync(userAccount, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
var response = new CreateAccountResponse(userAccount.Id, userAccount.FullName);
return Ok(response);
}
}
32 changes: 0 additions & 32 deletions src/Api/Controllers/WeatherForecastController.cs

This file was deleted.

14 changes: 6 additions & 8 deletions src/Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
var builder = WebApplication.CreateBuilder(args);
using Appointer.Infrastructure;

// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddInfrastructure(builder.Configuration);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

public partial class Program{}
public partial class Program
{
}
12 changes: 0 additions & 12 deletions src/Api/WeatherForecast.cs

This file was deleted.

3 changes: 3 additions & 0 deletions src/Domain/Accounts/UserAccount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Appointer.Domain.Accounts;

public record UserAccount(Guid Id, string FullName, bool IsActive = true, bool IsDeleted = false);
11 changes: 11 additions & 0 deletions src/Domain/Domain.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Appointer.Domain</AssemblyName>
<RootNamespace>Appointer.Domain</RootNamespace>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Appointer.Infrastructure.Entities;
using Appointer.Domain.Accounts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Appointer.Infrastructure.Configuration;

public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
public class UserAccountConfiguration : IEntityTypeConfiguration<UserAccount>
{
public void Configure(EntityTypeBuilder<TodoItem> builder)
public void Configure(EntityTypeBuilder<UserAccount> builder)
{
builder
.HasKey(x => x.Id);
Expand All @@ -16,15 +16,15 @@ public void Configure(EntityTypeBuilder<TodoItem> builder)
.ValueGeneratedNever();

builder
.Property(x => x.Title)
.Property(x => x.FullName)
.IsRequired();

builder
.Property(x => x.Description)
.HasMaxLength(2000);
.Property(x => x.IsActive)
.HasDefaultValue(true);

builder
.Property(x => x.Done)
.IsRequired();
.Property(x => x.IsDeleted)
.HasDefaultValue(false);
}
}
19 changes: 19 additions & 0 deletions src/Infrastructure/DbContext/AppointerDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Appointer.Domain.Accounts;
using Microsoft.EntityFrameworkCore;

namespace Appointer.Infrastructure.DbContext;

public class AppointerDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public AppointerDbContext(DbContextOptions<AppointerDbContext> options) : base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppointerDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}

public DbSet<UserAccount> UserAccounts { get; set; }
}
19 changes: 0 additions & 19 deletions src/Infrastructure/DbContext/TodoDbContext.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<TodoDbContext>(options =>
services.AddDbContext<AppointerDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("Database")));

return services;
Expand Down
3 changes: 0 additions & 3 deletions src/Infrastructure/Entities/TodoItem.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>

</Project>
19 changes: 15 additions & 4 deletions tests/Api.IntegrationTests/Accounts/AccountsShould.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
using System.Net;
using System.Net.Http.Json;
using Appointer.Api.IntegrationTests.Helpers;
using Appointer.Api.Requests;
using Appointer.Api.Responses;
using Appointer.Infrastructure.DbContext;
using FluentAssertions;
using FluentAssertions.Common;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace Appointer.Api.IntegrationTests.Accounts;

public class AccountsShould : IClassFixture<WebApplicationFactory<Program>>
public class AccountsShould : IClassFixture<AppointerWebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;

public AccountsShould(WebApplicationFactory<Program> factory)
public AccountsShould(AppointerWebApplicationFactory<Program> factory)
{
_factory = factory;
}

[Fact]
public async Task CreateAccount()
{
Expand All @@ -34,5 +38,12 @@ public async Task CreateAccount()
response.Should().NotBeNull();
response!.Id.Should().NotBeEmpty();
response.FullName.Should().Be(fullName);


using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppointerDbContext>();
var newItem = await dbContext.UserAccounts.FindAsync(response.Id);
newItem.Should().NotBeNull();
newItem.FullName.Should().Be(fullName);

Check warning on line 47 in tests/Api.IntegrationTests/Accounts/AccountsShould.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 47 in tests/Api.IntegrationTests/Accounts/AccountsShould.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}
}
}
2 changes: 2 additions & 0 deletions tests/Api.IntegrationTests/Api.IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Testcontainers" Version="3.8.0" />
<PackageReference Include="Testcontainers.MsSql" Version="3.8.0" />
<PackageReference Include="xunit" Version="2.7.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Appointer.Infrastructure.DbContext;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.MsSql;

namespace Appointer.Api.IntegrationTests.Helpers;

// ReSharper disable once ClassNeverInstantiated.Global
public class AppointerWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime
where TProgram : class
{
private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder()
.WithCleanUp(true)
.Build();

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.RemoveDbContext<AppointerDbContext>();
services.AddDbContext<AppointerDbContext>(options =>
{
options.UseSqlServer(_msSqlContainer.GetConnectionString());
});
services.EnsureDbCreated<AppointerDbContext>();
});
}

public async Task InitializeAsync() => await _msSqlContainer.StartAsync();

public new async Task DisposeAsync() => await _msSqlContainer.StopAsync();
}
22 changes: 22 additions & 0 deletions tests/Api.IntegrationTests/Helpers/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Appointer.Api.IntegrationTests.Helpers;

public static class ServiceCollectionExtensions
{
public static void RemoveDbContext<T>(this IServiceCollection services) where T : DbContext
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<T>));
if (descriptor != null) services.Remove(descriptor);
}

public static void EnsureDbCreated<T>(this IServiceCollection services) where T : DbContext
{
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<T>();
context.Database.EnsureCreated();
}
}
Loading

0 comments on commit 239cc19

Please sign in to comment.