Skip to content

Pin every test parameter, and let a harness name the ones that drive - #61

Merged
ipjohnson merged 2 commits into
mainfrom
pin-every-test-parameter
Sep 9, 2026
Merged

ipjohnson merged 2 commits into
mainfrom
pin-every-test-parameter

Conversation

@ipjohnson

Copy link
Copy Markdown
Owner

1.4.0 shipped the wrong default. This fixes it, and writes the rule down so it does not move a third time: What gets pinned.

What was wrong

1.4.0 pinned only what an attribute declared: [Mock] and [InjectValues] implement ISharedTestRegistration, [Shared] marks the rest. Coherent, and it survived every unit test here and 8341 in the consuming framework.

It failed on the first test a scaffolded project runs. That test takes a plain application class the handler appends to and asserts on what it recorded; with no attribute on it, it was rebuilt with the container the send ran on and the test read an empty one.

A rule that needs an attribute to work is a rule most tests will not get. A bare parameter is the common case, not the exception.

The rule now

A test parameter is one instance for the whole test, unless it is something the harness supplies to drive the application.

A registration nothing holds is per container, unless the harness pins it by name.

A parameter exists to be looked at, so pinning it needs no attribute. [Shared] on a value parameter still works and is now redundant, deliberately: it earns its place on a parameter that drives the application, where it asks for one container across the calls, and a reader should not have to know which kind of parameter they are looking at before writing it.

The one new member

ISharedTestRegistration.IsolatedServices(MethodInfo) names the parameters that must not be pinned, because they build containers rather than live in one: a trigger façade, ITestWebApp, HttpClient, a generated client. Only the harness supplying them can know which they are — they are ordinary types in an ordinary signature — and it answers per test method because the answer is a property of the signature rather than of the attribute.

It wins over an explicit [Shared]. Pinning one of these is not a preference that could go either way; it turns the isolation off while the test believes it is on.

IServiceProvider is never pinned either. A test asking for one is asking which container it is in, which is the one question pinning cannot answer.

Default implementation returning empty, so the public API diff is a single added member. Nothing removed, no signature changed.

Tests

SharedRegistrationsTests is rewritten as one test per row of the rule, twelve of them, because the way it was wrong last time was a case nobody had written down. The container-level tests gain ABareParameterIsTheSameObjectInEveryContainer, which is the shape that caught this.

EachContainerGetsItsOwnApplicationSingleton now asserts on a service the test does not take as a parameter, since taking one pins it. Without that change it would have quietly stopped testing anything.

Verified

  • dotnet build --configuration Release -p:ContinuousIntegrationBuild=true — clean.
  • scripts/coverage.sh 85 — 90.7%.
  • scripts/verify-packages.sh 1.5.0 — real consumers on net8.0 and net10.0.
  • Full suite green on both target frameworks.

1.4.0 was released hours ago and nothing consumed it, so the cost of superseding it is nil.

🤖 Generated with Claude Code

Ian Johnson and others added 2 commits September 8, 2026 20:35
1.4.0 pinned only what an attribute declared, and that is the wrong default. A
parameter exists to be looked at: the test was handed it so it could assert on
it, or configure it and then assert on something else. Handing the test one
container's instance while the invocation runs against another makes the
assertion meaningless, and it is meaningless the same way for a plain
application class a handler appends to as for a [Mock]. The attribute is not
what makes it so.

The rule is two sentences now. A test parameter is one instance for the whole
test, unless it is something the harness supplies to drive the application. A
registration nothing holds is per container, unless the harness pins it by name.

IsolatedServices(MethodInfo) is the second half. A trigger facade, a test web
application, an HttpClient and a generated client build a container per call, so
pinning one pins the thing meant to be rebuilt and every call runs against one
container while the test believes otherwise. Only the harness supplying them can
know which they are, and it answers per test method because the answer is a
property of the signature rather than of the attribute. It wins over an explicit
[Shared], because that is correctness rather than preference.

IServiceProvider is never pinned. A test asking for one is asking which
container it is in.

Found by the first test a scaffolded project runs, which takes a plain class and
asserts on what the handler recorded. A rule that needs an attribute to work is
a rule most tests will not get.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinning is a statement about identity, not a reason to construct something the
test never asked for. The set is drawn from a signature, so it holds types
nothing registered - a value from a data row, a concrete class the resolver
builds on the spot - alongside the ones that matter.

The case that made this necessary is sharper than an absent registration. A
harness may register a deliberately failing factory for a parameter it cannot
supply, so that resolving it fails with a message naming the fix. Resolving
eagerly turned that message into a failure at container build for every test
that took such a parameter and never resolved it, which a data-driven test whose
row supplies a string does. Twenty-four tests in the consuming framework failed
that way, none of them about anything this feature touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ipjohnson
ipjohnson merged commit 45153d5 into main Sep 9, 2026
2 checks passed
@ipjohnson
ipjohnson deleted the pin-every-test-parameter branch September 9, 2026 11:08
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