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
155 lines
6.0 KiB
C#
155 lines
6.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using NovelSoftware.Application;
|
|
using NovelSoftware.Application.Dtos;
|
|
using NovelSoftware.Domain;
|
|
|
|
namespace NovelSoftware.Tests;
|
|
|
|
[TestFixture]
|
|
public class ProjectDataTests : ServiceTestFixture
|
|
{
|
|
private async Task<Guid> NewProjectAsync() =>
|
|
(await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
|
|
|
|
[Test]
|
|
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
|
{
|
|
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"));
|
|
|
|
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: ""));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(afterClear.Genre, Is.Null);
|
|
Assert.That(afterClear.Logline, Is.EqualTo("A cartographer goes to sea."));
|
|
});
|
|
}
|
|
|
|
[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"));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(first.Number, Is.EqualTo(1));
|
|
Assert.That(second.Number, Is.EqualTo(2));
|
|
});
|
|
}
|
|
|
|
[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 scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
|
|
"The dock at dawn", Prose: "Five words go right here"));
|
|
|
|
Assert.That(scene.WordCount, Is.EqualTo(5));
|
|
|
|
var rewritten = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(
|
|
Prose: "Now\nthere are seven words in total"));
|
|
|
|
Assert.That(rewritten.WordCount, Is.EqualTo(7));
|
|
|
|
var cleared = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: ""));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(cleared.Prose, Is.Null);
|
|
Assert.That(cleared.WordCount, Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
[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(
|
|
"The dock at dawn", Prose: "The tide came in slow."));
|
|
|
|
var updated = await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: 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));
|
|
});
|
|
}
|
|
|
|
[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 Projects.DeleteAsync(projectId);
|
|
|
|
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));
|
|
});
|
|
}
|
|
|
|
[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 ines = await Characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines"));
|
|
var stranger = await Characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger"));
|
|
|
|
Assert.That(
|
|
async () => await Characters.AddRelationshipAsync(
|
|
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")),
|
|
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
|
|
}
|
|
|
|
[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 updated = await Characters.AddRelationshipAsync(
|
|
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", "Estranged since the fire."));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(updated.Relationships, Has.Count.EqualTo(1));
|
|
Assert.That(updated.Relationships[0].RelatedCharacterName, Is.EqualTo("Mara"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void Reading_a_missing_project_reports_not_found() =>
|
|
Assert.That(
|
|
async () => await Projects.GetAsync(Guid.NewGuid()),
|
|
Throws.TypeOf<NotFoundException>());
|
|
}
|