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:
James Wampler
2026-08-06 12:11:20 -07:00
co-authored by Claude Opus 5
parent 8394843255
commit 30e0c6926e
9 changed files with 510 additions and 434 deletions
@@ -1,4 +1,3 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using NovelSoftware.Application;
using NovelSoftware.Application.Agent;
@@ -6,21 +5,20 @@ using NovelSoftware.Infrastructure.Anthropic;
namespace NovelSoftware.Tests;
[TestFixture]
public class AnthropicClientTests
{
[Fact]
public void Constructing_without_a_key_does_not_throw()
{
[Test]
public void Constructing_without_a_key_does_not_throw() =>
// The agent service takes this as a dependency and also serves read-only endpoints
// (listing conversations, reading a transcript). Throwing at construction would
// take those down on any install that has not configured a key yet.
var construct = () => new AnthropicAgentModelClient(Options.Create(new AgentOptions()));
Assert.That(
() => new AnthropicAgentModelClient(Options.Create(new AgentOptions())),
Throws.Nothing);
construct.Should().NotThrow();
}
[Fact]
public async Task Sending_without_a_key_reports_a_configuration_problem()
[Test]
public void Sending_without_a_key_reports_a_configuration_problem()
{
var previous = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
Environment.SetEnvironmentVariable("ANTHROPIC_API_KEY", null);
@@ -29,10 +27,9 @@ public class AnthropicClientTests
{
var client = new AnthropicAgentModelClient(Options.Create(new AgentOptions()));
var send = async () => await client.CompleteAsync("system", [], []);
(await send.Should().ThrowAsync<AgentNotConfiguredException>())
.WithMessage("*ANTHROPIC_API_KEY*");
Assert.That(
async () => await client.CompleteAsync("system", [], []),
Throws.TypeOf<AgentNotConfiguredException>().With.Message.Contains("ANTHROPIC_API_KEY"));
}
finally
{