fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5678)#5682
fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5678)#5682
Conversation
The generated MockImpl constructor for partial/wrap mocks now carries [System.Diagnostics.CodeAnalysis.SetsRequiredMembers], so mocking a class with `required` members no longer triggers CS9035 in the factory. Adds T17 coverage in KitchenSinkEdgeCasesTests for both a single required reference type member and a class with multiple required members spanning reference and value types.
The attribute literal was repeated 6 times across GenerateWrapConstructors and GeneratePartialConstructors. Extract it to a single private const so the call sites become declarative and the (mildly long) global:: name exists in one place. No change to generated output; snapshot tests unchanged.
There was a problem hiding this comment.
Code Review — fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5678)
Summary
The fix is correct and well-targeted. Annotating every generated mock constructor with [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] is the right approach — it suppresses CS9035 at the mock-factory call site without requiring users to provide required-member values they don't care about in tests.
What's Good
Correctness of the attribute placement — Emitting [SetsRequiredMembers] on all generated constructors (not only when the mocked type is detected to have required members) is a defensively correct choice. It avoids the need to statically detect required members in the source generator, and since these constructors are internal to the generated mock implementation, there's no false-safety risk to callers.
No polyfill needed — Correctly noted in the PR description: net8.0+ ships the attribute natively, and the repo's Polyfill package covers net472. No extra work required.
Snapshot tests updated — All ten affected .verified.txt files correctly reflect the new attribute line. The snapshot coverage confirms the attribute appears on every ctor variant (default, parameterless, parameterized, wrap, partial).
Test design — Replacing the skipped T17 block with two concrete tests (single required string + multi-required string/int/Guid) gives good breadth. Mixing reference and value types in T17_Multiple_Required_Members_Reference_And_Value_Types is a meaningful edge-case check.
One Concern: Uninitialized Required Properties in Mock Instances
[SetsRequiredMembers] tells the compiler "this constructor takes responsibility for required members" — but the generated constructors don't actually assign them. The generated mock inherits the required fields from the base class, and they remain at their default values (null for strings, 0 / Guid.Empty for value types).
This is acceptable behaviour for a mock library (the mock is a test double, not a real object), but it's worth documenting or at least adding an assertion to the test to make the expectation explicit:
[Test]
public async Task T17_Required_Property_Does_Not_Block_Mock_Instantiation()
{
var mock = Mock.Create<RequiredShape>();
// Required members are intentionally left at their default values in mocks.
// The attribute [SetsRequiredMembers] suppresses CS9035 at the call site.
Assert.That(mock.Name, Is.Null); // documents the "uninitialized" contract
await Assert.That(mock.Compute()).IsEqualTo(0);
}This makes it clear to future contributors that null here is intentional, not a bug — and protects against a future refactor that accidentally initializes these properties to something unexpected.
Minor: Structural Repetition in MockImplBuilder.cs
The attribute emission appears six times (three in GenerateWrapConstructors, three in GeneratePartialConstructors), each as:
writer.WriteLine(SetsRequiredMembersAttribute);This is not harmful — it's a single constant write — but the structural pattern of "emit attribute, then emit constructor" is repeated across all six branch paths. If this file grows with future attributes (e.g. [Obsolete], [EditorBrowsable]), the per-branch duplication will compound.
A small local helper encapsulating the ctor preamble would make future extensions cleaner:
private static void WriteConstructorPreamble(IndentedTextWriter writer)
{
writer.WriteLine(SetsRequiredMembersAttribute);
// future: other ctor-level attributes go here
}This is a low-priority suggestion — the current structure is readable and the PR scope is already well-contained.
Verdict
The fix is sound, the snapshots are correct, and the tests cover the key shapes. The one actionable suggestion is to add an assertion that documents the "required members are left at default" contract. The structural duplication note is advisory only.
Good fix overall. The root-cause analysis in the PR description is accurate and the implementation matches it precisely.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…mock Reviewer noted that [SetsRequiredMembers] tells the compiler the constructor takes responsibility for required members, but the generated mock ctor intentionally leaves them at their default values. Add explicit assertions plus a one-line comment to T17 so future contributors see this is by design, not a missing feature.
Review feedback appliedApplied (commit
Deferred:
Verification (net10.0):
|
There was a problem hiding this comment.
Code Review — fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5682)
Follow-up on Prior Review (2026-04-23)
The previous review raised two concerns. Checking the current state:
✅ Uninitialized required properties — addressed. The tests now include both inline comments and explicit assertions documenting the contract:
// Required members intentionally remain at default in mocks; [SetsRequiredMembers] suppresses CS9035 only.
await Assert.That(mock.Object.Name).IsNull();
await Assert.That(mock.Object.Count).IsEqualTo(0);
await Assert.That(mock.Object.Id).IsEqualTo(System.Guid.Empty);This is exactly right. Future readers won't mistake null for a bug.
⬜ Structural repetition in MockImplBuilder.cs — still open (advisory). The six individual writer.AppendLine(SetsRequiredMembersAttribute) calls remain. Still low-priority — see below if this matters to you.
New Observations
Comment length exceeds project style guideline
MockImplBuilder.cs adds a 2-line comment block:
// Tells the compiler the generated ctor satisfies any `required` members on the
// mocked base type, so the factory can `new XxxMockImpl(engine)` without CS9035.CLAUDE.md says: "Don't write multi-paragraph docstrings or multi-line comment blocks — one short line max." The WHY here is genuinely non-obvious (a C# quirk, not self-evident from the code), so a comment is warranted — but it should be a single line:
// Suppresses CS9035: constructor claims responsibility for required members so the factory can new XxxMockImpl(engine) without initializers.Missing snapshot for a type with required members
The snapshot tests (TUnit.Mocks.SourceGenerator.Tests/Snapshots/) now all emit [SetsRequiredMembers], but none of the snapshot-test fixture types actually have required members. The runtime tests in KitchenSinkEdgeCasesTests.cs cover the compile+run path, but a dedicated snapshot — e.g. Class_With_Required_Members.verified.txt — would lock in the generated output for that exact case and make regressions immediately visible in the generator test suite. This is advisory, not blocking.
Overall Assessment
The fix is correct, well-scoped, and properly tested. The two new tests cover the meaningful edge cases (single required ref type; mixed ref + value types) and explicitly document the "required members remain at default" contract. Snapshot coverage is thorough for existing shapes. The main actionable items are:
- Minor (should fix): Collapse the 2-line comment in
MockImplBuilder.csto one line. - Advisory: Consider adding a snapshot fixture for a type with
requiredmembers. - Advisory: Consider a
WriteConstructorPreamblehelper if more ctor-level attributes are added in the future.
Good fix overall — the root cause is clearly understood and the implementation is precise.
Address 2nd-round review feedback on PR #5682: - Collapse 2-line comment on SetsRequiredMembersAttribute const to a single line per project style guideline (one short line max). - Add Class_With_Required_Members snapshot test that exercises the generator output for a partial-mocked base class with required ref-typed and value-typed properties, locking in the [SetsRequiredMembers] emission for the exact scenario fixed by #5678.
|
2nd-round review applied in b80c3c5:
Tests — all green on net10.0:
No |
There was a problem hiding this comment.
Code Review — Follow-up (Third Pass)
Prior Feedback Status
All actionable items from the two previous reviews have been addressed:
| Item | Status |
|---|---|
| Document "required members stay at default" contract in tests | ✅ Addressed in commit 1b0b89cc — explicit IsNull() / IsEqualTo(0) / IsEqualTo(Guid.Empty) assertions added |
| Collapse 2-line comment to one line | ✅ Addressed in commit b80c3c5a — now a single-line // Suppresses CS9035: ... |
Add snapshot for type with required members |
✅ Addressed in commit b80c3c5a — Class_With_Required_Members.verified.txt added |
Hoist SetsRequiredMembersAttribute to a constant |
✅ Addressed in commit b6edcbc1 |
Structural repetition (6 separate writer.AppendLine calls) |
⬜ Still open — always advisory, no action needed unless more ctor-level attributes are added |
Current State Assessment
The PR is in good shape. The fix is correct, the snapshot coverage is thorough, and the test comments/assertions make the "required members intentionally remain at default" contract explicit for future contributors. No outstanding actionable items remain.
The one open advisory (a WriteConstructorPreamble helper to centralise future ctor-level attribute emission) is low-priority and can be deferred until there is a second ctor-level attribute to justify the abstraction.
LGTM.
# Conflicts: # TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
|
Merged latest Conflict resolution (single hunk in
Snapshot reconciliation:
Verification (net10.0):
Merge commit: |
There was a problem hiding this comment.
Follow-up Review — Previous Feedback Status
This is a follow-up to the earlier review on this PR.
Previous Concerns — Addressed ✅
"Uninitialized Required Properties" concern (main feedback from prior review) — fully addressed. The tests now have both explicit assertions and explanatory comments:
// Required members intentionally remain at default in mocks; [SetsRequiredMembers] suppresses CS9035 only.
await Assert.That(mock.Object.Name).IsNull();
await Assert.That(mock.Object.Count).IsEqualTo(0);
await Assert.That(mock.Object.Id).IsEqualTo(System.Guid.Empty);This documents the contract clearly — future contributors won't mistake null for a bug.
Structural repetition note (advisory only, from prior review) — not acted upon, which is fine. The six direct writer.AppendLine(SetsRequiredMembersAttribute) calls remain. Still low-priority; acceptable as-is.
Fresh Assessment
The implementation is correct and complete:
- Attribute placement is unconditional — applied to all generated constructors regardless of whether the base type has
requiredmembers. This is the right call: it avoids adding static detection ofrequiredmembers to the source generator, and since these constructors areinternalto the generatedfile sealed class, there's no false-safety exposure. - Interface mock constructors also receive the attribute (see
Interface_With_Obsolete_Members.verified.txt). Interfaces can't declarerequiredmembers, so the attribute is technically a no-op there, but it's harmless and keeps the generator code unconditionally consistent — no special-casing needed. - Snapshot coverage is thorough: all ten affected
.verified.txtfiles are updated, covering default ctors, parameterless base ctors, parameterized ctors, partial mocks, and wrap mocks. - Test breadth is good: single
required string, and multi-required mixingstring/int/Guid(ref + value types).
No new concerns. This is a clean, minimal, correct fix.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.37.10 to 1.39.0. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.39.0 <!-- Release notes generated using configuration in .github/release.yml at v1.39.0 --> ## What's Changed ### Other Changes * perf(mocks): shrink MethodSetup + cache stateless matchers by @thomhurst in thomhurst/TUnit#5669 * fix(mocks): handle base classes with explicit interface impls (#5673) by @thomhurst in thomhurst/TUnit#5674 * fix(mocks): implement indexer in generated mock (#5676) by @thomhurst in thomhurst/TUnit#5683 * fix(mocks): disambiguate IEquatable<T>.Equals from object.Equals (#5675) by @thomhurst in thomhurst/TUnit#5680 * fix(mocks): escape C# keyword identifiers at all emit sites (#5679) by @thomhurst in thomhurst/TUnit#5684 * fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5678) by @thomhurst in thomhurst/TUnit#5682 * fix(mocks): skip MockBridge for class targets with static-abstract interfaces (#5677) by @thomhurst in thomhurst/TUnit#5681 * chore(mocks): regenerate source generator snapshots by @thomhurst in thomhurst/TUnit#5691 * perf(engine): collapse async state-machine layers on hot test path (#5687) by @thomhurst in thomhurst/TUnit#5690 * perf(engine): reduce lock contention in scheduling and hook caches (#5686) by @thomhurst in thomhurst/TUnit#5693 * fix(assertions): prevent implicit-to-string op from NREing on null (#5692) by @thomhurst in thomhurst/TUnit#5696 * perf(engine/core): reduce per-test allocations (#5688) by @thomhurst in thomhurst/TUnit#5694 * perf(engine): reduce message-bus contention on test start (#5685) by @thomhurst in thomhurst/TUnit#5695 ### Dependencies * chore(deps): update tunit to 1.37.36 by @thomhurst in thomhurst/TUnit#5667 * chore(deps): update verify to 31.16.2 by @thomhurst in thomhurst/TUnit#5699 **Full Changelog**: thomhurst/TUnit@v1.37.36...v1.39.0 ## 1.37.36 <!-- Release notes generated using configuration in .github/release.yml at v1.37.36 --> ## What's Changed ### Other Changes * fix(telemetry): remove duplicate HTTP client spans by @thomhurst in thomhurst/TUnit#5668 **Full Changelog**: thomhurst/TUnit@v1.37.35...v1.37.36 ## 1.37.35 <!-- Release notes generated using configuration in .github/release.yml at v1.37.35 --> ## What's Changed ### Other Changes * Add TUnit.TestProject.Library to the TUnit.Dev.slnx solution file by @Zodt in thomhurst/TUnit#5655 * fix(aspire): preserve user-supplied OTLP endpoint (#4818) by @thomhurst in thomhurst/TUnit#5665 * feat(aspire): emit client spans for HTTP by @thomhurst in thomhurst/TUnit#5666 ### Dependencies * chore(deps): update dependency dotnet-sdk to v10.0.203 by @thomhurst in thomhurst/TUnit#5656 * chore(deps): update microsoft.aspnetcore to 10.0.7 by @thomhurst in thomhurst/TUnit#5657 * chore(deps): update tunit to 1.37.24 by @thomhurst in thomhurst/TUnit#5659 * chore(deps): update microsoft.extensions to 10.0.7 by @thomhurst in thomhurst/TUnit#5658 * chore(deps): update aspire to 13.2.3 by @thomhurst in thomhurst/TUnit#5661 * chore(deps): update dependency microsoft.net.test.sdk to 18.5.0 by @thomhurst in thomhurst/TUnit#5664 ## New Contributors * @Zodt made their first contribution in thomhurst/TUnit#5655 **Full Changelog**: thomhurst/TUnit@v1.37.24...v1.37.35 ## 1.37.24 <!-- Release notes generated using configuration in .github/release.yml at v1.37.24 --> ## What's Changed ### Other Changes * docs: add Tluma Ask AI widget to Docusaurus site by @thomhurst in thomhurst/TUnit#5638 * Revert "chore(deps): update dependency docusaurus-plugin-llms to ^0.4.0 (#5637)" by @thomhurst in thomhurst/TUnit#5640 * fix(asp-net): forward disposal in FlowSuppressingHostedService (#5651) by @JohnVerheij in thomhurst/TUnit#5652 ### Dependencies * chore(deps): update dependency docusaurus-plugin-llms to ^0.4.0 by @thomhurst in thomhurst/TUnit#5637 * chore(deps): update tunit to 1.37.10 by @thomhurst in thomhurst/TUnit#5639 * chore(deps): update opentelemetry to 1.15.3 by @thomhurst in thomhurst/TUnit#5645 * chore(deps): update opentelemetry by @thomhurst in thomhurst/TUnit#5647 * chore(deps): update dependency dompurify to v3.4.1 by @thomhurst in thomhurst/TUnit#5648 * chore(deps): update dependency system.commandline to 2.0.7 by @thomhurst in thomhurst/TUnit#5650 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.7 by @thomhurst in thomhurst/TUnit#5649 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.203 by @thomhurst in thomhurst/TUnit#5653 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.203 by @thomhurst in thomhurst/TUnit#5654 **Full Changelog**: thomhurst/TUnit@v1.37.10...v1.37.24 Commits viewable in [compare view](thomhurst/TUnit@v1.37.10...v1.39.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
[SetsRequiredMembers]on every generatedXxxMockImpl/XxxWrapMockImplconstructor so the factory cannewthem without the user having to satisfyrequiredmembers at the call site.T17_Required_Property_Does_Not_Block_Mock_Instantiationand adds a multi-required shape (ref + value type) so the initializer-skip logic is exercised broadly.Why
A class with
requiredmembers (e.g.public required string Name { get; init; }) couldn't be mocked: the generated factory'snew XxxMockImpl(engine)produced CS9035 because the constructor didn't declare it took responsibility for the required members. Annotating the generated ctor with[SetsRequiredMembers]tells the compiler the constructor satisfies the contract — which is appropriate for a mock that is itself the test double.No polyfill was needed: net8.0+ ships the attribute, and the repo's
Polyfillpackage supplies it for net472.Test plan
TUnit.Mocks.Testsfull suite (net10.0): 954/954TUnit.Mocks.SourceGenerator.Tests(net10.0): 45/45Closes #5678