diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index ad61c62f9c..23eafab4d0 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 SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf() { 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_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.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/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; 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 4ecadf46bf..9834275dc6 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -5,6 +5,9 @@ namespace TUnit.Mocks.SourceGenerator.Builders; internal static class MockImplBuilder { + // 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) { var safeName = GetCompositeShortSafeName(model); @@ -151,6 +154,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo if (model.Constructors.Length == 0) { + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance)")) { writer.AppendLine("_engine = engine;"); @@ -167,6 +171,7 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo { if (ctor.Parameters.Length == 0) { + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance) : base()")) { writer.AppendLine("_engine = engine;"); @@ -181,6 +186,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(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}WrapMockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine, {model.FullyQualifiedName} wrappedInstance, {paramList}) : base({argList})")) { writer.AppendLine("_engine = engine;"); @@ -496,6 +502,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (model.Constructors.Length == 0) { // No explicit constructors found, generate a default one + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine)")) { writer.AppendLine("_engine = engine;"); @@ -512,6 +519,7 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel if (ctor.Parameters.Length == 0) { // Parameterless constructor + writer.AppendLine(SetsRequiredMembersAttribute); using (writer.Block($"internal {safeName}MockImpl(global::TUnit.Mocks.MockEngine<{mockableType}> engine) : base()")) { writer.AppendLine("_engine = engine;"); @@ -526,6 +534,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(SetsRequiredMembersAttribute); 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 6c80ed6a16..4855fe9530 100644 --- a/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs +++ b/TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs @@ -201,11 +201,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 @@ -570,7 +581,39 @@ 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); + + // Required members intentionally remain at default in mocks; [SetsRequiredMembers] suppresses CS9035 only. + await Assert.That(mock.Object.Name).IsNull(); + } + + [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); + + // 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.