Migrate the test suite from xUnit + FluentAssertions to NUnit
All 44 tests, same names, same coverage — verified by diffing the runner's test
list before and after.
The load-bearing change is fixture lifecycle. xUnit builds a new test-class
instance per test, so a `readonly TestDatabase _db = new()` field gave every
test its own database. NUnit reuses one instance for the whole class, so those
field initialisers and constructors would have shared a single database across a
class and let tests read each other's rows. Setup moved into [SetUp]/[TearDown]
via a new ServiceTestFixture base class, which also collapses the per-class
service wiring that was duplicated five times.
Assertions are now Assert.That with the constraint model:
Should().Be(x) -> Is.EqualTo(x)
Should().BeNull() -> Is.Null
Should().HaveCount(n) -> Has.Count.EqualTo(n)
Should().Equal(a, b) -> Is.EqualTo(new[] { a, b })
Should().BeEquivalentTo(..) -> Is.EquivalentTo(..)
Should().Contain("x") -> Does.Contain("x")
Should().OnlyHaveUniqueItems() -> Is.Unique
ThrowAsync<T>().WithMessage("*m*")
-> Throws.TypeOf<T>().With.Message.Contains("m")
FluentAssertions' `.Which` chains became plain indexed asserts, grouped in
Assert.Multiple so a failure reports every broken expectation in the case rather
than stopping at the first.
Because a framework migration can quietly produce vacuously-passing tests,
spot-checked four conversions by mutation — breaking the code under an async
Assert.Multiple block, a sync one, a Throws constraint, and a collection
ordering assert. All four failed as they should, confirming the assertions are
live and that NUnit bound the async lambdas to AsyncTestDelegate rather than
silently accepting them as async void.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
This commit is contained in:
co-authored by
Claude Opus 5
parent
8394843255
commit
30e0c6926e
@@ -1,154 +1,154 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NovelSoftware.Application;
|
||||
using NovelSoftware.Application.Dtos;
|
||||
using NovelSoftware.Application.Services;
|
||||
using NovelSoftware.Domain;
|
||||
|
||||
namespace NovelSoftware.Tests;
|
||||
|
||||
public class ProjectDataTests : IDisposable
|
||||
[TestFixture]
|
||||
public class ProjectDataTests : ServiceTestFixture
|
||||
{
|
||||
private readonly TestDatabase _db = new();
|
||||
private readonly TagService _tags;
|
||||
private readonly ProjectService _projects;
|
||||
private readonly CharacterService _characters;
|
||||
private readonly ChapterService _chapters;
|
||||
private readonly SceneService _scenes;
|
||||
|
||||
public ProjectDataTests()
|
||||
{
|
||||
_tags = new TagService(_db.Context);
|
||||
_projects = new ProjectService(_db.Context);
|
||||
_characters = new CharacterService(_db.Context, _tags);
|
||||
_chapters = new ChapterService(_db.Context, _tags);
|
||||
_scenes = new SceneService(_db.Context);
|
||||
}
|
||||
|
||||
private async Task<Guid> NewProjectAsync() =>
|
||||
(await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
|
||||
(await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
||||
{
|
||||
var id = (await _projects.CreateAsync(
|
||||
var id = (await Projects.CreateAsync(
|
||||
new CreateProjectRequest("Draft", Genre: "Fantasy", Logline: "A cartographer goes to sea."))).Id;
|
||||
|
||||
var afterPartialUpdate = await _projects.UpdateAsync(id, new UpdateProjectRequest(Title: "The Salt Road"));
|
||||
var afterPartialUpdate = await Projects.UpdateAsync(id, new UpdateProjectRequest(Title: "The Salt Road"));
|
||||
|
||||
afterPartialUpdate.Title.Should().Be("The Salt Road");
|
||||
afterPartialUpdate.Genre.Should().Be("Fantasy");
|
||||
afterPartialUpdate.Logline.Should().Be("A cartographer goes to sea.");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(afterPartialUpdate.Title, Is.EqualTo("The Salt Road"));
|
||||
Assert.That(afterPartialUpdate.Genre, Is.EqualTo("Fantasy"));
|
||||
Assert.That(afterPartialUpdate.Logline, Is.EqualTo("A cartographer goes to sea."));
|
||||
});
|
||||
|
||||
var afterClear = await _projects.UpdateAsync(id, new UpdateProjectRequest(Genre: ""));
|
||||
var afterClear = await Projects.UpdateAsync(id, new UpdateProjectRequest(Genre: ""));
|
||||
|
||||
afterClear.Genre.Should().BeNull();
|
||||
afterClear.Logline.Should().Be("A cartographer goes to sea.");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(afterClear.Genre, Is.Null);
|
||||
Assert.That(afterClear.Logline, Is.EqualTo("A cartographer goes to sea."));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Chapters_are_numbered_in_sequence_when_no_number_is_given()
|
||||
{
|
||||
var projectId = await NewProjectAsync();
|
||||
|
||||
var first = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
var second = await _chapters.CreateAsync(projectId, new CreateChapterRequest("The Harbour"));
|
||||
var first = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
var second = await Chapters.CreateAsync(projectId, new CreateChapterRequest("The Harbour"));
|
||||
|
||||
first.Number.Should().Be(1);
|
||||
second.Number.Should().Be(2);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(first.Number, Is.EqualTo(1));
|
||||
Assert.That(second.Number, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Word_count_is_recomputed_whenever_prose_changes()
|
||||
{
|
||||
var projectId = await NewProjectAsync();
|
||||
var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
|
||||
var scene = await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
||||
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
||||
"The dock at dawn", Prose: "Five words go right here"));
|
||||
|
||||
scene.WordCount.Should().Be(5);
|
||||
Assert.That(scene.WordCount, Is.EqualTo(5));
|
||||
|
||||
var rewritten = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(
|
||||
var rewritten = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(
|
||||
Prose: "Now\nthere are seven words in total"));
|
||||
|
||||
rewritten.WordCount.Should().Be(7);
|
||||
Assert.That(rewritten.WordCount, Is.EqualTo(7));
|
||||
|
||||
var cleared = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: ""));
|
||||
var cleared = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: ""));
|
||||
|
||||
cleared.Prose.Should().BeNull();
|
||||
cleared.WordCount.Should().Be(0);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(cleared.Prose, Is.Null);
|
||||
Assert.That(cleared.WordCount, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Scene_updates_that_omit_prose_leave_the_draft_untouched()
|
||||
{
|
||||
var projectId = await NewProjectAsync();
|
||||
var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
var scene = await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
||||
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
||||
"The dock at dawn", Prose: "The tide came in slow."));
|
||||
|
||||
var updated = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: DraftStatus.Revised));
|
||||
var updated = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: DraftStatus.Revised));
|
||||
|
||||
updated.Prose.Should().Be("The tide came in slow.");
|
||||
updated.WordCount.Should().Be(5);
|
||||
updated.Status.Should().Be(DraftStatus.Revised);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(updated.Prose, Is.EqualTo("The tide came in slow."));
|
||||
Assert.That(updated.WordCount, Is.EqualTo(5));
|
||||
Assert.That(updated.Status, Is.EqualTo(DraftStatus.Revised));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Deleting_a_project_takes_its_characters_chapters_and_scenes()
|
||||
{
|
||||
var projectId = await NewProjectAsync();
|
||||
await _characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
||||
var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest("The dock at dawn"));
|
||||
await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
||||
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
|
||||
await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest("The dock at dawn"));
|
||||
|
||||
await _projects.DeleteAsync(projectId);
|
||||
await Projects.DeleteAsync(projectId);
|
||||
|
||||
using var verification = _db.CreateContext();
|
||||
(await verification.Projects.CountAsync()).Should().Be(0);
|
||||
(await verification.Characters.CountAsync()).Should().Be(0);
|
||||
(await verification.Chapters.CountAsync()).Should().Be(0);
|
||||
(await verification.Scenes.CountAsync()).Should().Be(0);
|
||||
using var verification = Db.CreateContext();
|
||||
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await verification.Projects.CountAsync(), Is.EqualTo(0));
|
||||
Assert.That(await verification.Characters.CountAsync(), Is.EqualTo(0));
|
||||
Assert.That(await verification.Chapters.CountAsync(), Is.EqualTo(0));
|
||||
Assert.That(await verification.Scenes.CountAsync(), Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Relating_characters_across_projects_is_refused()
|
||||
{
|
||||
var firstProject = await NewProjectAsync();
|
||||
var secondProject = (await _projects.CreateAsync(new CreateProjectRequest("Other Book"))).Id;
|
||||
var secondProject = (await Projects.CreateAsync(new CreateProjectRequest("Other Book"))).Id;
|
||||
|
||||
var ines = await _characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines"));
|
||||
var stranger = await _characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger"));
|
||||
var ines = await Characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines"));
|
||||
var stranger = await Characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger"));
|
||||
|
||||
var relate = async () => await _characters.AddRelationshipAsync(
|
||||
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister"));
|
||||
|
||||
await relate.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*same project*");
|
||||
Assert.That(
|
||||
async () => await Characters.AddRelationshipAsync(
|
||||
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Relationships_resolve_the_other_character_by_name()
|
||||
{
|
||||
var projectId = await NewProjectAsync();
|
||||
var ines = await _characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
||||
var mara = await _characters.CreateAsync(projectId, new CreateCharacterRequest("Mara"));
|
||||
var ines = await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
|
||||
var mara = await Characters.CreateAsync(projectId, new CreateCharacterRequest("Mara"));
|
||||
|
||||
var updated = await _characters.AddRelationshipAsync(
|
||||
var updated = await Characters.AddRelationshipAsync(
|
||||
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", "Estranged since the fire."));
|
||||
|
||||
updated.Relationships.Should().ContainSingle()
|
||||
.Which.RelatedCharacterName.Should().Be("Mara");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(updated.Relationships, Has.Count.EqualTo(1));
|
||||
Assert.That(updated.Relationships[0].RelatedCharacterName, Is.EqualTo("Mara"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reading_a_missing_project_reports_not_found()
|
||||
{
|
||||
var get = async () => await _projects.GetAsync(Guid.NewGuid());
|
||||
|
||||
await get.Should().ThrowAsync<NotFoundException>();
|
||||
}
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
[Test]
|
||||
public void Reading_a_missing_project_reports_not_found() =>
|
||||
Assert.That(
|
||||
async () => await Projects.GetAsync(Guid.NewGuid()),
|
||||
Throws.TypeOf<NotFoundException>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user