Add integration and unit tests for Notes API with PostgreSQL-based test hosting configuration
This commit is contained in:
70
Freman.Sample.Web.IntegrationTests/Api/NotesApiTests.cs
Normal file
70
Freman.Sample.Web.IntegrationTests/Api/NotesApiTests.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Freman.Sample.Web.Contracts;
|
||||
using Shouldly;
|
||||
using Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.Api;
|
||||
|
||||
[Collection(IntegrationTestCollection.Name)]
|
||||
public class NotesApiTests(IntegrationTestFixture fixture) : IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client = fixture.Factory.CreateClient();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await fixture.ResetDatabaseAsync();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task GetNotes_InitiallyEmpty_Returns200AndEmptyList()
|
||||
{
|
||||
var resp = await _client.GetAsync("/api/notes", TestContext.Current.CancellationToken);
|
||||
resp.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var notes = await resp.Content.ReadFromJsonAsync<List<NoteDto>>(cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostNote_ThenGetNotes_ReturnsTheNewNote()
|
||||
{
|
||||
var uniqueText = $"test note {Guid.NewGuid():N}";
|
||||
|
||||
var post = await _client.PostAsJsonAsync("/api/notes",
|
||||
new CreateNoteRequest { Text = uniqueText },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
post.StatusCode.ShouldBe(HttpStatusCode.Created);
|
||||
|
||||
var notes = await _client.GetFromJsonAsync<List<NoteDto>>("/api/notes", cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Any(n => n.Text == uniqueText).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteNote_RemovesIt()
|
||||
{
|
||||
var post = await _client.PostAsJsonAsync("/api/notes", new CreateNoteRequest { Text = "delete me" },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
var created = await post.Content.ReadFromJsonAsync<NoteDto>(cancellationToken: TestContext.Current.CancellationToken);
|
||||
created.ShouldNotBeNull();
|
||||
|
||||
var del = await _client.DeleteAsync($"/api/notes/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
del.StatusCode.ShouldBe(HttpStatusCode.NoContent);
|
||||
|
||||
var notes = await _client.GetFromJsonAsync<List<NoteDto>>("/api/notes", cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Any(n => n.Id == created.Id).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteMissingNote_Returns404()
|
||||
{
|
||||
var del = await _client.DeleteAsync("/api/notes/123456", TestContext.Current.CancellationToken);
|
||||
del.StatusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"/>
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Freman.Sample.Web.Contracts\Freman.Sample.Web.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>
|
||||
{
|
||||
public const string Name = "Integration tests";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Freman.Sample.Web.Data;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
public sealed class IntegrationTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder("postgres:16")
|
||||
.WithDatabase("testdb")
|
||||
.WithUsername("appuser")
|
||||
.WithPassword("testpassword")
|
||||
.Build();
|
||||
|
||||
public SampleWebFactory Factory { get; private set; } = null!;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _pg.StartAsync();
|
||||
|
||||
Factory = new SampleWebFactory(_pg.GetConnectionString());
|
||||
|
||||
// Apply migrations once
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public new async ValueTask DisposeAsync()
|
||||
{
|
||||
await Factory.DisposeAsync();
|
||||
await _pg.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ResetDatabaseAsync()
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.ExecuteSqlRawAsync(@"TRUNCATE TABLE notes RESTART IDENTITY;");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
public sealed class SampleWebFactory(string connectionString) : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
var overrides = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:AppDb"] = connectionString,
|
||||
["ApplyMigrationsOnStartup"] = "false"
|
||||
};
|
||||
|
||||
config.AddInMemoryCollection(overrides);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user