Skip to content

Take DependencyModules' [Mock], and offer NUnit, Moq and FakeItEasy in the templates - #284

Merged
ipjohnson merged 1 commit into
mainfrom
template-test-frameworks
Sep 5, 2026
Merged

ipjohnson merged 1 commit into
mainfrom
template-test-frameworks

Conversation

@ipjohnson

Copy link
Copy Markdown
Owner

Summary

[Mock] is DependencyModules' attribute now. Hardened.Shared.Testing carried its own MockAttribute, hardwired to NSubstitute.Substitute.For, with NSubstitute as a package dependency. DependencyModules has had the pluggable form since the split into per-library packages: its [Mock] builds nothing itself and asks the IMockSupportAttribute in scope, which DependencyModules.NSubstitute, DependencyModules.Moq and DependencyModules.FakeItEasy provide as [NSubstituteSupport], [MoqSupport] and [FakeItEasySupport]. That migration was owed when the split landed and did not happen in #92, #201, #273 or #281. This makes it.

On top of it, every template takes two new options: --test-framework xunit|nunit and --mocks nsubstitute|moq|fakeiteasy. The defaults are xUnit and NSubstitute, so a bare dotnet new scaffolds what it did before.

Breaking

  • [Mock] moved from Hardened.Shared.Testing.Attributes to DependencyModules.Testing.Attributes, and it needs a support attribute in scope. A test project on 0.20 adds one package reference (DependencyModules.NSubstitute at the DependencyModules line the framework is on) and one line beside its entry point: [assembly: NSubstituteSupport]. Without the attribute a [Mock] parameter fails with "Mock library not found" rather than handing back the real service.
  • Hardened.Shared.Testing no longer brings NSubstitute. A project that used it transitively references it, or the DependencyModules package, itself.

The repository's own consumers are moved: the two Web integration test projects, Hardened.Shared.Testing.Tests and Hardened.Shared.Testing.NUnit.Tests. MockAttributeTests tested the deleted class and goes with it; ParameterInjectionTests covers [Mock] through the resolver under the new attribute. The approved public API for Hardened.Shared.Testing loses the class.

What each option changes in a generated project

--test-framework nunit swaps the runner package (Hardened.Shared.Testing.NUnit for .xUnit), the framework packages (NUnit and NUnit3TestAdapter for xunit.v3 and xunit.runner.visualstudio), drops the <OutputType>Exe</OutputType> xUnit v3 needs, changes the global using, and writes every assertion in NUnit's constraint form. [HardenedTest] reads the same on both, which is what the runner split was for.

--mocks swaps one package reference and one assembly attribute in Bootstrap.cs, and writes the sample that resolves a mock for that library: [Mock] T with Arg.Any/Returns for NSubstitute, a Mock<T> parameter with Setup for Moq, [Mock] T with A.CallTo for FakeItEasy.

The web template gains that sample: TodoStoreMockTests puts a mock ITodoStore behind GET /todos/{id} through ITestWebApp, so it is the same file whichever client was chosen. The library sample's [Mock] test is written the three ways. The function sample has no seam to mock, so there the option only swaps the package and the attribute.

The generated Directory.Packages.props pins the mock package at $(DependencyModulesVersion), stamped at pack from the property the framework builds against, the same way HardenedVersion is. A literal would have gone stale the way the RazorBlade snippet did. stage-templates.py stamps both tokens and fails if either reached nothing.

The README and AGENTS.md of each template follow the options: the runner paragraph, the mocking paragraph naming the library and the attribute, and the assertion lines in every sample.

Verification

scripts/verify-templates.sh combos carry two more fields, host:contract[:model[:client[:tests[:mocks]]]]. Three web rows leave the defaults, chosen so each of NUnit, Moq and FakeItEasy compiles and passes on a client variant that carries the fork: kestrel:code:response:kiota:nunit, kestrel:openapi:response:refit:xunit:moq, kestrel:code:throws:none:nunit:fakeiteasy. The library runs four ways and the function template two more. For every row, check_test_options holds the generated test project to the runner and the mock library it asked for and none of the others, on package references and the assembly attribute; run_tests requires the run to report tests passed, since dotnet test answers 0 for a project that discovered none, and then runs the mock test by name so a support attribute that failed to reach the output cannot pass. check_generated also fails on either version token reaching the output unstamped.

Locally: the solution builds under the CI flags, all 7261 tests pass, and the full verify-templates.sh run is green: 16 web rows including both Smithy rows, 4 library rows, 5 function and Lambda rows, the dotted-name, host-independence and alias checks, and the named mock test passing in all 21 projects that carry it.

Docs

The mocks guide says NSubstitute arrives with Hardened.Shared.Testing, and the testing guide, the attributes reference and the templates page's option tables need the two options. That is a Hardened.Docs change, queued behind #32.

🤖 Generated with Claude Code

…n the templates

Hardened.Shared.Testing's own MockAttribute was hardwired to NSubstitute and carried it as a
dependency, a duplicate of the pluggable [Mock] DependencyModules ships with its per-library
support packages. It goes, with the NSubstitute reference; a test project now names its library
with [assembly: NSubstituteSupport], [MoqSupport] or [FakeItEasySupport] and the matching package.

Every template takes --test-framework xunit|nunit and --mocks nsubstitute|moq|fakeiteasy, with
xUnit and NSubstitute the defaults. The verification script runs each of the other three at
least once and holds every generated test project to the runner and library it asked for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ipjohnson
ipjohnson merged commit 65c9192 into main Sep 5, 2026
2 checks passed
ipjohnson added a commit that referenced this pull request Sep 5, 2026
)

A code-first handler's operationId was the method name in camelCase, so
the Refit client from a code-first template said All() where the
spec-first one said ListTodos(), and the template's Refit tests carried a
codeFirst branch for the names. [Operation("listTodos")] on the handler
declares the id; the document writes it as given, a derived name that
collides with a declared one is prefixed with its tag as two derived
names are, and two handlers declaring one id is HRDOA004. Spec-first
handlers now carry their description's id verbatim rather than a
camel-cased round trip, which is why the Smithy fixture's export moves
to its shape names.

The template's code-first sample declares the contract's four ids, and
the same constraints: [Range(Min = 1)] on the id and [StringLength(1, 64)]
on the title, so the code-first document declares the same 400s with the
same RequestValidationError schema. The Refit method names, the
constraint tests and the malformed-id tests therefore run in every
contract mode with no branch, and DocumentStatusTests has one expectation
set for every mode that declares its statuses.

The socket test's Kiota and Refit blocks gain the NUnit assert pair #284
put around every other assert; merged after it, #283 had left them
xUnit-only, so the script's kiota:nunit row did not compile.

Co-authored-by: Ian Johnson <ianjohnson@mac.mynetworksettings.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@ipjohnson
ipjohnson deleted the template-test-frameworks branch September 6, 2026 00:04
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