Neutral test seam, an NUnit integration, and conventions folded into the main generator - #35
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, inDependencyModules.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, andModuleTestCasedropped ~100 lines, keeping only what is genuinely xUnit's.2. An NUnit integration
DependencyModules.NUnitships 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. OnlyModuleTestAttributeis per-framework, because each has to derive from what its own framework requires.Two behaviours worth calling out:
[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.[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.SourceGeneratorThe separate analyzer package is gone. Its contracts now ship as public types in
DependencyModules.RuntimeunderDependencyModules.Runtime.Conventions, rather than being emitted into every consuming compilation — which is what forced them to beinternal, 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:
[ModuleTest], assembly-level modules, container-per-test, parameter resolution order,[TestExport],[InjectValues]IDataAttributerows,FactAttributeproperties,ITestCaseInfo[ModuleTestCase], iteration lifetime, fixture model[Mock]plus NSubstitute, Moq and FakeItEasy in turnStale references to the removed conventions package are gone throughout, including the
dotnet add package DependencyModules.Conventionsstep and the claim that an implicitConventionsmethod fails to compile.Incidental fix:
IServiceProviderBuilderAttributeprecedenceBoth 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 needsusing DependencyModules.Testing.Attributes;alongside the one for[ModuleTest].DependencyModules.Conventionsis no longer published. Conventions need nothing beyondDependencyModules.RuntimeandDependencyModules.SourceGenerator.ModuleTestCasedirectly: the constructor gainedskipExceptionsin fifth position.Verification
Release build warning-free. Full suite on both
net8.0andnet10.0: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