Skip to content

Neutral test seam, an NUnit integration, and conventions folded into the main generator - #35

Merged
ipjohnson merged 3 commits into
mainfrom
refactor/neutral-parameter-resolver
Aug 11, 2026
Merged

Neutral test seam, an NUnit integration, and conventions folded into the main generator#35
ipjohnson merged 3 commits into
mainfrom
refactor/neutral-parameter-resolver

Conversation

@ipjohnson

@ipjohnson ipjohnson commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Three commits that share one seam and are easier to review together. The first two are the groundwork; the third is the bulk.

1. A framework-neutral test seam (4176218, 499aa63)

Turning a test method's parameter list into arguments is the same problem whichever framework runs the test: an attribute on the parameter may supply the value, or it comes from the container, or it is keyed, or the type is an unregistered concrete class the container can still build. All of it lived inside ModuleTestCase, so a second integration would have reimplemented it.

TestParameterResolver, in DependencyModules.Testing. Used in two phases either side of the container being built — setup while registrations can still be added, resolution once there is a provider. That lifecycle is what the rules already depended on but never stated. Resolving without the setup phase now throws rather than silently skipping every parameter attribute, which would have handed a [Mock] parameter the real service.

[Mock] moved there too, and ModuleTestCase dropped ~100 lines, keeping only what is genuinely xUnit's.

2. An NUnit integration

DependencyModules.NUnit ships the same [ModuleTest] contract — name your modules, take services as method parameters. The seam above is what makes it plumbing rather than a second engine: [Mock], [InjectValues], [TestExport], keyed services and the resolution rules are all shared, so both integrations hand out the same attributes rather than copies. Only ModuleTestAttribute is per-framework, because each has to derive from what its own framework requires.

Two behaviours worth calling out:

  • A container per test iteration, not per test case. Each [Repeat] pass and [Retry] attempt builds and disposes its own, and it brackets the whole iteration — [SetUp] → method → [TearDown] — so both run while it is alive. Wrapping only the invocation would have left [SetUp] running before the container existed.
  • Data rows use [ModuleTestCase]. NUnit's [TestCase] requires a row to supply an argument for every parameter and enforces that when the case is built, before this package runs, so it cannot express "row covers the leading parameters, container covers the rest". It also builds its own cases, so combining the two would produce a case per row plus one more.

3. Conventions fold into DependencyModules.SourceGenerator

The separate analyzer package is gone. Its contracts now ship as public types in DependencyModules.Runtime under DependencyModules.Runtime.Conventions, rather than being emitted into every consuming compilation — which is what forced them to be internal, forced explicit interface implementation, and made CS0436 unavoidable between two assemblies that both emitted them.

Consequence worth knowing: an implicit public void Conventions(…) now compiles and is matched, alongside the explicit form. The explicit one still wins if a type somehow carries both.

4. Docs restructured

The testing section now separates what is shared from what is not:

Page Holds
Testing modules The neutral core — [ModuleTest], assembly-level modules, container-per-test, parameter resolution order, [TestExport], [InjectValues]
xUnit Only what differs — IDataAttribute rows, FactAttribute properties, ITestCaseInfo
NUnit Only what differs — [ModuleTestCase], iteration lifetime, fixture model
Mocking frameworks [Mock] plus NSubstitute, Moq and FakeItEasy in turn

Stale references to the removed conventions package are gone throughout, including the dotnet add package DependencyModules.Conventions step and the claim that an implicit Conventions method fails to compile.

Incidental fix: IServiceProviderBuilderAttribute precedence

Both integrations took the first match from an attribute list ordered widest-scope-first, so an assembly-level container builder silently beat one declared on the class or the method — the reverse of the interface's own documentation, and of how every other test attribute here resolves. A test asking for a particular container was overridden by a project-wide default with nothing to indicate it.

The narrowest declaration now wins. Only one builder is ever used; that is unchanged, so a project declaring exactly one at a single scope sees no difference.

Breaking, pre-1.0

  • [Mock] and [TestExport] move namespace. A test file needs using DependencyModules.Testing.Attributes; alongside the one for [ModuleTest].
  • DependencyModules.Conventions is no longer published. Conventions need nothing beyond DependencyModules.Runtime and DependencyModules.SourceGenerator.
  • Anyone constructing ModuleTestCase directly: the constructor gained skipExceptions in fifth position.

Verification

Release build warning-free. Full suite on both net8.0 and net10.0:

  • 690 unit
  • 135 xUnit integration
  • 34 NUnit integration
  • 2 web

The precedence fix was checked against its own test by reverting it — one failure per framework, Expected: "method" / Actual: "class".

🤖 Generated with Claude Code

https://claude.ai/code/session_011DCTxUFQq3n74aDTLZUQt6

Ian Johnson and others added 3 commits August 10, 2026 07:57
Turning a test method's parameter list into arguments is the same problem
whichever test framework is running it: an attribute on the parameter may
supply the value, or it comes from the container, or it is keyed, or the
type is an unregistered concrete class the container can still build. All
of that lived inside ModuleTestCase, so an NUnit integration would have
had to reimplement it rather than reuse it.

Extract it as TestParameterResolver in DependencyModules.Testing. It is
used in two phases either side of the container being built — setup while
registrations can still be added, resolution once there is a provider —
which is the lifecycle the rules already depended on but never stated.
Resolving without the setup phase now throws instead of silently skipping
every parameter attribute, which would have handed a [Mock] parameter the
real service.

Behaviour is unchanged, including the order candidates are tried in and
the rule that a data row's own arguments cover the leading parameters. The
132 integration tests are the evidence; they exercise [Mock],
[InjectValues], keyed services, [TestExport] and data-driven rows through
the real pipeline. The rules now also have direct tests, which previously
were only reachable by running a [ModuleTest] end to end.

[Mock] moves to DependencyModules.Testing in the same change. Once the
hooks it implements stopped naming xUnit it had no test framework
dependency left, so a future integration gets the same attribute rather
than a copy of it. It joins [InjectValues], which was already there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sz9Gt6Ne33LdBStt1cKVyn
[TestExport] and [MoqSupport] both register through ITestServiceSetupAttribute,
so they run in one pass and the later registration wins. Attributes reach that
pass widest scope first, so declaration order alone would hand the outcome to
whichever happens to sit nearer the method. ModuleTestCase sorts mock support to
the front to stop that, and nothing tested it.

The existing MoqAttributeTests case has [MoqSupport] on the class and
[TestExport] on the method — an arrangement where declaration order already
agrees with the intended precedence, so it passes with the sort removed. This
covers the inverted arrangement, which is the one the sort exists for, and fails
without it.

Verified by removing the sort: this test fails, the existing one still passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sz9Gt6Ne33LdBStt1cKVyn
Three strands of work that share a seam and are easier to read together.

**An NUnit integration.** `DependencyModules.NUnit` ships the same `[ModuleTest]`
contract the xUnit integration has: name your modules, take services as method
parameters. Everything framework-neutral is genuinely shared rather than
reimplemented — `[Mock]`, `[InjectValues]`, `[TestExport]`, keyed services and
the parameter resolution rules all moved into `DependencyModules.Testing`, so
both integrations hand out the same attributes rather than copies. A container
is built and torn down per test *iteration*, bracketing `[SetUp]`/`[TearDown]`,
so a `[Repeat]` pass never inherits a mutated singleton. Data rows use
`[ModuleTestCase]`, because NUnit's own `[TestCase]` requires every parameter to
come from the row and enforces that before this package runs.

**Conventions fold into `DependencyModules.SourceGenerator`.** The separate
analyzer package is gone. Its contracts now ship as public types in
`DependencyModules.Runtime` under `DependencyModules.Runtime.Conventions`
instead of being emitted into each consuming compilation, which is what forced
them to be internal and made CS0436 unavoidable between two assemblies that both
emitted them. An implicit `public void Conventions(…)` consequently compiles now,
alongside the explicit form.

**Docs restructured around the two axes.** The testing section splits shared
concepts from per-framework detail: `Testing modules` holds the neutral core,
`xUnit` and `NUnit` hold only what differs, and `Mocking frameworks` covers
`[Mock]` plus NSubstitute, Moq and FakeItEasy in turn. Stale references to the
removed conventions package are gone throughout.

Also fixes `IServiceProviderBuilderAttribute` precedence: both integrations took
the first match from a widest-scope-first list, so an assembly-level container
builder silently beat one declared on the method. The narrowest now wins, matching
the interface's documentation and how every other test attribute resolves, with a
test pinning it in both frameworks.

Release build is warning-free; 690 unit, 135 xUnit integration, 34 NUnit
integration and 2 web tests pass on net8.0 and net10.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DCTxUFQq3n74aDTLZUQt6
@ipjohnson ipjohnson changed the title Share test parameter resolution and [Mock] across test frameworks Neutral test seam, an NUnit integration, and conventions folded into the main generator Aug 11, 2026
@ipjohnson
ipjohnson merged commit 315943a into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant