From 70600a457588c2ca24b659caeaabda5c70794cbf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:35:22 +0100 Subject: [PATCH 1/5] fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#5678) 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. --- ...arameters_Extension_Discovery.verified.txt | 3 ++ ...e_Arity_Constructor_Overloads.verified.txt | 4 ++ ...Attribute_With_Concrete_Class.verified.txt | 1 + ...terface_With_Obsolete_Members.verified.txt | 1 + ...embers_From_External_Assembly.verified.txt | 1 + ...With_Internal_Signature_Types.verified.txt | 1 + ...Inaccessible_Property_Setters.verified.txt | 1 + ...c_Constrained_Virtual_Methods.verified.txt | 1 + ...embers_From_External_Assembly.verified.txt | 1 + ...c_Constrained_Virtual_Methods.verified.txt | 1 + .../Builders/MockImplBuilder.cs | 6 +++ .../KitchenSinkEdgeCasesTests.cs | 47 ++++++++++++++++--- 12 files changed, 62 insertions(+), 6 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Constructor_Parameters_Extension_Discovery.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Constructor_Parameters_Extension_Discovery.verified.txt index 08d9c6c884..9219227ce4 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Constructor_Parameters_Extension_Discovery.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Constructor_Parameters_Extension_Discovery.verified.txt @@ -10,14 +10,17 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, string connectionString, int timeout) : base(connectionString, timeout) { _engine = engine; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, string connectionString, int timeout, bool verbose) : base(connectionString, timeout, verbose) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Same_Arity_Constructor_Overloads.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Same_Arity_Constructor_Overloads.verified.txt index c0852b973a..3a2564e389 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Same_Arity_Constructor_Overloads.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Same_Arity_Constructor_Overloads.verified.txt @@ -10,18 +10,22 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, string name) : base(name) { _engine = engine; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, int id) : base(id) { _engine = engine; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, string host, int port) : base(host, port) { _engine = engine; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine, int timeout, bool verbose) : base(timeout, verbose) { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/GenerateMock_Attribute_With_Concrete_Class.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/GenerateMock_Attribute_With_Concrete_Class.verified.txt index 70eee77d19..3439ef3b15 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/GenerateMock_Attribute_With_Concrete_Class.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/GenerateMock_Attribute_With_Concrete_Class.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt index 1afb836cfc..75c955cf57 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Obsolete_Members.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal BaseDialogMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt index faad323967..fb3c5eb9c1 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated.ExternalLib [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal ExternalClientMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt index 51eef9d8cd..58be91736b 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Filters_Members_With_Internal_Signature_Types.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated.ExternalLib [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal ServiceClientMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Omits_Inaccessible_Property_Setters.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Omits_Inaccessible_Property_Setters.verified.txt index 2f1debed02..fceed0883d 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Omits_Inaccessible_Property_Setters.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_Omits_Inaccessible_Property_Setters.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated.ExternalLib [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal ExternalResponseMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt index a9fa6b6db0..1894f961d5 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Partial_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal BaseServiceMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt index b8485e4f70..b81b6b45f7 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_Filters_Internal_Virtual_Members_From_External_Assembly.verified.txt @@ -11,6 +11,7 @@ namespace TUnit.Mocks.Generated.ExternalLib [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal ExternalServiceWrapMockImpl(global::TUnit.Mocks.MockEngine engine, global::ExternalLib.ExternalService wrappedInstance) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt index 2edd07f51c..1d5618a378 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Wrap_Mock_With_Generic_Constrained_Virtual_Methods.verified.txt @@ -11,6 +11,7 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal RepositoryWrapMockImpl(global::TUnit.Mocks.MockEngine engine, global::Repository wrappedInstance) : base() { _engine = engine; diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 9795c2d090..76e38f8cb7 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -139,6 +139,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo if (model.Constructors.Length == 0) { + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance)")) { writer.AppendLine("_engine = engine;"); @@ -155,6 +156,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo { if (ctor.Parameters.Length == 0) { + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance) : base()")) { writer.AppendLine("_engine = engine;"); @@ -169,6 +171,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo { var paramList = string.Join(", ", ctor.Parameters.Select(p => $"{p.FullyQualifiedType} {p.Name}")); var argList = string.Join(", ", ctor.Parameters.Select(p => p.Name)); + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance, {paramList}) : base({argList})")) { writer.AppendLine("_engine = engine;"); @@ -478,6 +481,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (model.Constructors.Length == 0) { // No explicit constructors found, generate a default one + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine)")) { writer.AppendLine("_engine = engine;"); @@ -494,6 +498,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (ctor.Parameters.Length == 0) { // Parameterless constructor + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine) : base()")) { writer.AppendLine("_engine = engine;"); @@ -508,6 +513,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel // Constructor with parameters - pass them through to base var paramList = string.Join(", ", ctor.Parameters.Select(p => $"{p.FullyQualifiedType} {p.Name}")); var argList = string.Join(", ", ctor.Parameters.Select(p => p.Name)); + writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {paramList}) : base({argList})")) { writer.AppendLine("_engine = engine;"); diff --git a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs index 0421541d1c..46b0a7a570 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -180,11 +180,22 @@ public interface ICancellableStream IAsyncEnumerable Stream(CancellationToken ct = default); } -// ─── T17 SKIPPED. `required` members on a mock target produce CS9035 -// ("required member must be set in the object initializer") in the -// generated factory. The factory would need to emit [SetsRequiredMembers] -// on its constructor and skip initializing required members. Separate -// generator fix. +// ─── T17. `required` members on a mock target ─────────────────────────────── + +public abstract class RequiredShape +{ + public required string Name { get; init; } + public abstract int Compute(); +} + +public abstract class RequiredMixed +{ + public required string Title { get; init; } + public required int Count { get; init; } + public required System.Guid Id { get; init; } + public abstract string Describe(); + public virtual int Bonus() => 0; +} // ─── T18 SKIPPED. Member names matching C# keywords (`class`, `event`, `record`) // are passed through to the generator as unescaped identifiers, producing @@ -462,7 +473,31 @@ static async IAsyncEnumerable Yield(params int[] values) } } - // T17 test elided — see the SKIPPED note above the type declarations. + // ── T17 ── + + [Test] + public async Task T17_Required_Property_Does_Not_Block_Mock_Instantiation() + { + var mock = RequiredShape.Mock(); + mock.Compute().Returns(123); + + await Assert.That(mock.Object.Compute()).IsEqualTo(123); + mock.Compute().WasCalled(Times.Once); + } + + [Test] + public async Task T17_Multiple_Required_Members_Reference_And_Value_Types() + { + var mock = RequiredMixed.Mock(); + mock.Describe().Returns("hello"); + mock.Bonus().Returns(7); + + await Assert.That(mock.Object.Describe()).IsEqualTo("hello"); + await Assert.That(mock.Object.Bonus()).IsEqualTo(7); + + mock.Describe().WasCalled(Times.Once); + mock.Bonus().WasCalled(Times.Once); + } // T18 test elided — see the SKIPPED note above the type declarations. From b6edcbc153c3fa7a8264b3d22011a489849a69e1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:38:39 +0100 Subject: [PATCH 2/5] refactor(mocks): hoist [SetsRequiredMembers] string to a const 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. --- .../Builders/MockImplBuilder.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 76e38f8cb7..d3045a8207 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -5,6 +5,10 @@ namespace TUnit.Mocks.SourceGenerator.Builders; internal static class MockImplBuilder { + // Tells the compiler the generated ctor satisfies any `required` members on the + // mocked base type, so the factory can `new XxxMockImpl(engine)` without CS9035. + private const string SetsRequiredMembersAttribute = "[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"; + public static void BuildInto(CodeWriter writer, MockTypeModel model) { var safeName = GetCompositeShortSafeName(model); @@ -139,7 +143,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo if (model.Constructors.Length == 0) { - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance)")) { writer.AppendLine("_engine = engine;"); @@ -156,7 +160,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo { if (ctor.Parameters.Length == 0) { - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance) : base()")) { writer.AppendLine("_engine = engine;"); @@ -171,7 +175,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo { var paramList = string.Join(", ", ctor.Parameters.Select(p => $"{p.FullyQualifiedType} {p.Name}")); var argList = string.Join(", ", ctor.Parameters.Select(p => p.Name)); - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance, {paramList}) : base({argList})")) { writer.AppendLine("_engine = engine;"); @@ -481,7 +485,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (model.Constructors.Length == 0) { // No explicit constructors found, generate a default one - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine)")) { writer.AppendLine("_engine = engine;"); @@ -498,7 +502,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (ctor.Parameters.Length == 0) { // Parameterless constructor - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine) : base()")) { writer.AppendLine("_engine = engine;"); @@ -513,7 +517,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel // Constructor with parameters - pass them through to base var paramList = string.Join(", ", ctor.Parameters.Select(p => $"{p.FullyQualifiedType} {p.Name}")); var argList = string.Join(", ", ctor.Parameters.Select(p => p.Name)); - writer.AppendLine("[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"); + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {paramList}) : base({argList})")) { writer.AppendLine("_engine = engine;"); From 1b0b89cc211e6349c11afb14621118c2a5669c88 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:50:50 +0100 Subject: [PATCH 3/5] test(mocks): document default-state contract for required members on 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. --- TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs index 46b0a7a570..683f8e183b 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -483,6 +483,9 @@ public async Task T17_Required_Property_Does_Not_Block_Mock_Instantiation() await Assert.That(mock.Object.Compute()).IsEqualTo(123); mock.Compute().WasCalled(Times.Once); + + // Required members intentionally remain at default in mocks; [SetsRequiredMembers] suppresses CS9035 only. + await Assert.That(mock.Object.Name).IsNull(); } [Test] @@ -497,6 +500,11 @@ public async Task T17_Multiple_Required_Members_Reference_And_Value_Types() mock.Describe().WasCalled(Times.Once); mock.Bonus().WasCalled(Times.Once); + + // Required members intentionally remain at default in mocks; [SetsRequiredMembers] suppresses CS9035 only. + await Assert.That(mock.Object.Title).IsNull(); + await Assert.That(mock.Object.Count).IsEqualTo(0); + await Assert.That(mock.Object.Id).IsEqualTo(System.Guid.Empty); } // T18 test elided — see the SKIPPED note above the type declarations. From b80c3c5ab24193d01cbe793f8546c94c6aaa4cd3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:03:55 +0100 Subject: [PATCH 4/5] chore(mocks): tighten comment, add required-members snapshot 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. --- .../MockGeneratorTests.cs | 27 ++++++ .../Class_With_Required_Members.verified.txt | 84 +++++++++++++++++++ .../Builders/MockImplBuilder.cs | 3 +- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Required_Members.verified.txt diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index 2e7891ae62..adc02db235 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -1347,6 +1347,33 @@ void M() return VerifyGeneratorOutput(source); } + [Test] + public Task Class_With_Required_Members() + { + // Regression for #5678: when a partial-mocked base class has `required` members, + // the generated impl ctor must carry [SetsRequiredMembers] so the factory can + // `new XxxMockImpl(engine)` without CS9035 (member must be initialized). + var source = """ + using TUnit.Mocks; + + public class ConfigBase + { + public required string Name { get; set; } + public required object Settings { get; init; } + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of(); + } + } + """; + + return VerifyGeneratorOutput(source); + } + [Test] public Task Interface_Inheriting_Nested_Generic_IEnumerable() { diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Required_Members.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Required_Members.verified.txt new file mode 100644 index 0000000000..31065db736 --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Class_With_Required_Members.verified.txt @@ -0,0 +1,84 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + file sealed class ConfigBaseMockImpl : global::ConfigBase, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject + { + private readonly global::TUnit.Mocks.MockEngine _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] + internal ConfigBaseMockImpl(global::TUnit.Mocks.MockEngine engine) : base() + { + _engine = engine; + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class ConfigBasePartialMockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory(Create); + } + + private static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new ConfigBaseMockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class ConfigBase_MockMemberExtensions + { + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class ConfigBase_MockStaticExtension + { + extension(global::ConfigBase _) + { + public static global::TUnit.Mocks.Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return global::TUnit.Mocks.Mock.Of(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index d3045a8207..d04783ff03 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -5,8 +5,7 @@ namespace TUnit.Mocks.SourceGenerator.Builders; internal static class MockImplBuilder { - // Tells the compiler the generated ctor satisfies any `required` members on the - // mocked base type, so the factory can `new XxxMockImpl(engine)` without CS9035. + // Suppresses CS9035: ctor claims responsibility for required members so factory can `new XxxMockImpl(engine)` without initializers. private const string SetsRequiredMembersAttribute = "[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]"; public static void BuildInto(CodeWriter writer, MockTypeModel model) From 0e51359b9d7674e33042bbac81e16c3f0414d360 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:52:58 +0100 Subject: [PATCH 5/5] test(mocks): update SelfEquatable snapshot for SetsRequiredMembers After merging main, the SelfEquatable snapshot from #5680 needs the [SetsRequiredMembers] attribute that this branch (#5678) emits on every generated impl ctor. --- ...able_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt index 72e6bbdcba..64fd05b306 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf.verified.txt @@ -10,6 +10,7 @@ namespace TUnit.Mocks.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] internal SelfEquatableSnapshotMockImpl(global::TUnit.Mocks.MockEngine engine) : base() { _engine = engine;