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,159 +1,163 @@
|
||||
using FluentAssertions;
|
||||
using NovelSoftware.Application;
|
||||
using NovelSoftware.Application.Dtos;
|
||||
using NovelSoftware.Application.Services;
|
||||
|
||||
namespace NovelSoftware.Tests;
|
||||
|
||||
public class BeatServiceTests : IDisposable
|
||||
[TestFixture]
|
||||
public class BeatServiceTests : ServiceTestFixture
|
||||
{
|
||||
private readonly TestDatabase _db = new();
|
||||
private readonly BeatService _beats;
|
||||
private readonly CharacterService _characters;
|
||||
private readonly SceneService _scenes;
|
||||
private readonly Guid _projectId;
|
||||
private readonly Guid _chapterId;
|
||||
private Guid _projectId;
|
||||
private Guid _chapterId;
|
||||
|
||||
public BeatServiceTests()
|
||||
protected override void OnSetUp()
|
||||
{
|
||||
var tags = new TagService(_db.Context);
|
||||
var projects = new ProjectService(_db.Context);
|
||||
var chapters = new ChapterService(_db.Context, tags);
|
||||
_characters = new CharacterService(_db.Context, tags);
|
||||
_scenes = new SceneService(_db.Context);
|
||||
_beats = new BeatService(_db.Context, tags);
|
||||
|
||||
_projectId = projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
|
||||
_chapterId = chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")).Result.Id;
|
||||
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
|
||||
_chapterId = Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")).Result.Id;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Beats_are_appended_in_order_and_listed_that_way()
|
||||
{
|
||||
await _beats.CreateAsync(_chapterId, new CreateBeatRequest("She finds the map"));
|
||||
await _beats.CreateAsync(_chapterId, new CreateBeatRequest("The harbour burns"));
|
||||
await _beats.CreateAsync(_chapterId, new CreateBeatRequest("She boards anyway"));
|
||||
await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She finds the map"));
|
||||
await Beats.CreateAsync(_chapterId, new CreateBeatRequest("The harbour burns"));
|
||||
await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She boards anyway"));
|
||||
|
||||
var listed = await _beats.ListAsync(_chapterId);
|
||||
var listed = await Beats.ListAsync(_chapterId);
|
||||
|
||||
listed.Select(b => b.Title)
|
||||
.Should().Equal("She finds the map", "The harbour burns", "She boards anyway");
|
||||
listed.Select(b => b.SortOrder).Should().Equal(1, 2, 3);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
listed.Select(b => b.Title),
|
||||
Is.EqualTo(new[] { "She finds the map", "The harbour burns", "She boards anyway" }));
|
||||
Assert.That(listed.Select(b => b.SortOrder), Is.EqualTo(new[] { 1, 2, 3 }));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Reordering_renumbers_to_match_the_order_given()
|
||||
{
|
||||
var first = await _beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
var second = await _beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
|
||||
var third = await _beats.CreateAsync(_chapterId, new CreateBeatRequest("Third"));
|
||||
var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
var second = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
|
||||
var third = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Third"));
|
||||
|
||||
var reordered = await _beats.ReorderAsync(
|
||||
var reordered = await Beats.ReorderAsync(
|
||||
_chapterId, new ReorderBeatsRequest([third.Id, first.Id, second.Id]));
|
||||
|
||||
reordered.Select(b => b.Title).Should().Equal("Third", "First", "Second");
|
||||
Assert.That(
|
||||
reordered.Select(b => b.Title),
|
||||
Is.EqualTo(new[] { "Third", "First", "Second" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Beats_left_out_of_a_reorder_keep_their_relative_position_at_the_end()
|
||||
{
|
||||
var first = await _beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
await _beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
|
||||
var third = await _beats.CreateAsync(_chapterId, new CreateBeatRequest("Third"));
|
||||
var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
|
||||
var third = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Third"));
|
||||
|
||||
var reordered = await _beats.ReorderAsync(_chapterId, new ReorderBeatsRequest([third.Id, first.Id]));
|
||||
var reordered = await Beats.ReorderAsync(_chapterId, new ReorderBeatsRequest([third.Id, first.Id]));
|
||||
|
||||
reordered.Select(b => b.Title).Should().Equal("Third", "First", "Second");
|
||||
Assert.That(
|
||||
reordered.Select(b => b.Title),
|
||||
Is.EqualTo(new[] { "Third", "First", "Second" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reordering_with_an_unknown_beat_is_refused()
|
||||
[Test]
|
||||
public void Reordering_with_an_unknown_beat_is_refused()
|
||||
{
|
||||
await _beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
Beats.CreateAsync(_chapterId, new CreateBeatRequest("First")).Wait();
|
||||
|
||||
var reorder = async () => await _beats.ReorderAsync(
|
||||
_chapterId, new ReorderBeatsRequest([Guid.NewGuid()]));
|
||||
|
||||
await reorder.Should().ThrowAsync<NotFoundException>();
|
||||
Assert.That(
|
||||
async () => await Beats.ReorderAsync(_chapterId, new ReorderBeatsRequest([Guid.NewGuid()])),
|
||||
Throws.TypeOf<NotFoundException>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task A_beat_resolves_its_character_and_scene_names()
|
||||
{
|
||||
var ines = await _characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
|
||||
var scene = await _scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
|
||||
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
|
||||
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
|
||||
|
||||
var beat = await _beats.CreateAsync(_chapterId, new CreateBeatRequest(
|
||||
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
|
||||
"She burns the atlas",
|
||||
CharacterId: ines.Id,
|
||||
WhatHappened: "The pages go up faster than she expected.",
|
||||
WhatsNext: "Nothing to navigate by but memory.",
|
||||
SceneId: scene.Id));
|
||||
|
||||
beat.CharacterName.Should().Be("Ines");
|
||||
beat.SceneTitle.Should().Be("The dock at dawn");
|
||||
beat.WhatHappened.Should().Contain("faster than she expected");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beat.CharacterName, Is.EqualTo("Ines"));
|
||||
Assert.That(beat.SceneTitle, Is.EqualTo("The dock at dawn"));
|
||||
Assert.That(beat.WhatHappened, Does.Contain("faster than she expected"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task A_beat_cannot_borrow_a_character_from_another_project()
|
||||
{
|
||||
var projects = new ProjectService(_db.Context);
|
||||
var other = await projects.CreateAsync(new CreateProjectRequest("Other Book"));
|
||||
var stranger = await _characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
|
||||
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
|
||||
var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
|
||||
|
||||
var create = async () => await _beats.CreateAsync(
|
||||
_chapterId, new CreateBeatRequest("A beat", CharacterId: stranger.Id));
|
||||
|
||||
await create.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*same project*");
|
||||
Assert.That(
|
||||
async () => await Beats.CreateAsync(
|
||||
_chapterId, new CreateBeatRequest("A beat", CharacterId: stranger.Id)),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task A_beat_cannot_be_grouped_under_a_scene_from_another_chapter()
|
||||
{
|
||||
var chapters = new ChapterService(_db.Context, new TagService(_db.Context));
|
||||
var elsewhere = await chapters.CreateAsync(_projectId, new CreateChapterRequest("Elsewhere"));
|
||||
var scene = await _scenes.CreateAsync(elsewhere.Id, new CreateSceneRequest("Another scene"));
|
||||
var elsewhere = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Elsewhere"));
|
||||
var scene = await Scenes.CreateAsync(elsewhere.Id, new CreateSceneRequest("Another scene"));
|
||||
|
||||
var create = async () => await _beats.CreateAsync(
|
||||
_chapterId, new CreateBeatRequest("A beat", SceneId: scene.Id));
|
||||
|
||||
await create.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*same chapter*");
|
||||
Assert.That(
|
||||
async () => await Beats.CreateAsync(
|
||||
_chapterId, new CreateBeatRequest("A beat", SceneId: scene.Id)),
|
||||
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same chapter"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Deleting_a_scene_leaves_its_beats_alone()
|
||||
{
|
||||
var scene = await _scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
|
||||
var beat = await _beats.CreateAsync(
|
||||
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
|
||||
var beat = await Beats.CreateAsync(
|
||||
_chapterId, new CreateBeatRequest("She burns the atlas", SceneId: scene.Id));
|
||||
|
||||
await _scenes.DeleteAsync(scene.Id);
|
||||
await Scenes.DeleteAsync(scene.Id);
|
||||
|
||||
// The plan outlives a decision about prose — the beat is simply ungrouped.
|
||||
var survivor = await _beats.GetAsync(beat.Id);
|
||||
survivor.SceneId.Should().BeNull();
|
||||
survivor.Title.Should().Be("She burns the atlas");
|
||||
var survivor = await Beats.GetAsync(beat.Id);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(survivor.SceneId, Is.Null);
|
||||
Assert.That(survivor.Title, Is.EqualTo("She burns the atlas"));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
||||
{
|
||||
var beat = await _beats.CreateAsync(_chapterId, new CreateBeatRequest(
|
||||
"She finds the map", WhatHappened: "Behind the lining of the case.", WhatsNext: "She books passage."));
|
||||
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
|
||||
"She finds the map",
|
||||
WhatHappened: "Behind the lining of the case.",
|
||||
WhatsNext: "She books passage."));
|
||||
|
||||
var renamed = await _beats.UpdateAsync(beat.Id, new UpdateBeatRequest(Title: "She finds it"));
|
||||
var renamed = await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(Title: "She finds it"));
|
||||
|
||||
renamed.WhatHappened.Should().Be("Behind the lining of the case.");
|
||||
renamed.WhatsNext.Should().Be("She books passage.");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(renamed.WhatHappened, Is.EqualTo("Behind the lining of the case."));
|
||||
Assert.That(renamed.WhatsNext, Is.EqualTo("She books passage."));
|
||||
});
|
||||
|
||||
var cleared = await _beats.UpdateAsync(beat.Id, new UpdateBeatRequest(WhatsNext: ""));
|
||||
var cleared = await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(WhatsNext: ""));
|
||||
|
||||
cleared.WhatsNext.Should().BeNull();
|
||||
cleared.WhatHappened.Should().Be("Behind the lining of the case.");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(cleared.WhatsNext, Is.Null);
|
||||
Assert.That(cleared.WhatHappened, Is.EqualTo("Behind the lining of the case."));
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user