Compono.NUnit¶
NUnit integration — a TestAttribute/ITestBuilder-implementing [Compose] attribute that composes test method parameters directly, without needing [TestFixture] on the containing class or a separate [Test] attribute.
When to install¶
You write NUnit tests and want method parameters composed automatically:
Compono.NUnit doesn't add an NUnit test host for you — it integrates with an existing one (both classic VSTest and Microsoft Testing Platform are supported — see "Runner support" below). [Compose] alone makes a method a real, independently discovered NUnit test — no [TestFixture] on the containing class, and no separate [Test] attribute, are required or expected:
public class OrderServiceTests
{
[Compose]
public void Creates_service(
SomeService sut,
[Shared] SomeDependency dependency)
{
}
}
This is the single most important thing to know about Compono.NUnit, because it's the opposite of Compono.XunitV3/Compono.TUnit/ Compono.MSTest — those need a separate [Fact]/[Test]/[TestMethod] alongside [Compose]; Compono.NUnit never does, and adding [TestFixture] "just in case" is harmless but unnecessary. See ADR-0059 for the full design and PLAN-0059 for implementation status.
What it gives you¶
[Compose]— every method parameter is composed; the method itself needs no other test-identifying attribute.- Inline + composed mixing —
[Compose(42, "widget")]binds inline values left-to-right; anything left over is composed. [Shared]— reuse one composed instance across every parameter (or nested dependency) in the same row that requests the same type. See Shared Values.Seed—[Compose(Seed = ...)]reproduces a specific composed row exactly. A row's seed is always surfaced in the test's display name (ComposedMethod(Compono, seed: 12345)), and a composition failure's message includes the seed that produced it. An unpinned seed is fresh on everyBuildFrominvocation — under classic VSTest, a separately-invoked discovery session followed by a separate execution session can therefore see two different unpinned seeds for the same eventual test case; pinSeedfor a reliably reproducible row.[Compose<TProfile>]— applies a fixed, default-constructed profile to the row'sComposer, matchingCompono.XunitV3/Compono.TUnit/Compono.MSTest's ownComposeAttribute<TProfile>exactly.[Compose<TProfile, TConfig>]— constructsTConfigfrom this attribute's own constructor arguments, then constructs and appliesTProfilefrom thatTConfig— a distinct binding target from ordinary inline values; every test-method parameter is still composed in full.
[Compose] and NUnit's own data sources coexist, never merge¶
One [Compose]-family attribute owns its entire row. NUnit's own [TestCase], [Values], [Range], and any custom IParameterDataSource-implementing attribute keep producing their own, completely independent test case(s) on the same method — never merged with [Compose]'s row, and never suppressed by it either:
[Compose]
[TestCase(42)]
public void Mixed(int value)
{
}
// -> two independent test cases: one composed, one literal (42)
[Compose]
public void WithValues([Values(1, 2, 3)] int value)
{
}
// -> four independent test cases: one composed (Compose ignores [Values]
// entirely) plus three from [Values] itself (1, 2, 3)
Runner support¶
Both classic VSTest (NUnit3TestAdapter + Microsoft.NET.Test.Sdk) and Microsoft Testing Platform (<EnableNUnitRunner>true</EnableNUnitRunner>) are supported — Compono.NUnit itself is runner-neutral and takes no dependency on either; which one runs your tests is entirely your test project's own choice.
Discovery-time module initialization. Under MTP your test assembly is the entry assembly, so the [ModuleInitializer]s the generator emits into it (which register row-invoker dispatch and composition plans) run during normal .NET startup, before any test is discovered. The classic VSTest test host instead builds test cases before running them, so Compono.NUnit forces the declaring test method's module constructor at the start of BuildFrom to guarantee those registrations exist by the time binding happens. This is reflection-free, idempotent, and Native-AOT-safe; it adds no public API. See ADR-0059 Amendment 1.
Supported NUnit versions¶
NUnit >= 3.14.0, < 5.0.0 — one compiled Compono.NUnit package supports the full range; there is no Compono.NUnit3/Compono.NUnit4 split. NUnit 5 is prerelease as of this writing and is tracked as forward-compatibility surveillance, not a current support target; the range will widen once NUnit 5.0.0 ships stable. See ADR-0059 §3/§5 for the evidence.
Composition boundaries¶
- Synchronous only — composition happens inside
BuildFrom, which NUnit calls synchronously; there is no async composition path. Async resource setup belongs in NUnit's own[OneTimeSetUp]/[SetUp]lifecycle, with the already-initialized resource registered into Compono synchronously. - Non-owning —
Compono.NUnitnever disposes a composed value. NUnit's own[TearDown]/[OneTimeTearDown]/IDisposablefixture lifecycle remains your own disposal seam. TestContextstays NUnit's —Compono.NUnitnever composes or special-casesTestContext.CurrentContext; it's NUnit's own static/ambient accessor, not a parameter Compono ever injects.- Composition may run more than once for one eventual test case — under classic VSTest, a separately-invoked discovery session followed by a separate execution session each independently call
BuildFrom. A registration factory orICompositionValueProvidermay therefore run more than once for what appears to be a single test.[Shared]/Share<T>()remain correct within each independently-built row.
Native AOT / trimming¶
Compono.NUnit's own integration code is reflection-free/trim-safe on the same terms as Compono.XunitV3/Compono.TUnit/Compono.MSTest — framework-mandated MethodInfo/IMethodInfo access only, no MakeGenericType/Activator.CreateInstance/dynamic generic activation. Whether NUnit's own runner/adapter chain is itself Native-AOT-runnable is a separate, narrower claim this package does not make either way — see ADR-0059 §17.