Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfigBase>();
}
}
""";

return VerifyGeneratorOutput(source);
}

[Test]
public Task SelfEquatable_Generates_EqualsOf_GetHashCodeOf_ToStringOf()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::MyService> engine) : base()
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine<global::MyService> engine, string connectionString, int timeout) : base(connectionString, timeout)
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine<global::MyService> engine, string connectionString, int timeout, bool verbose) : base(connectionString, timeout, verbose)
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// <auto-generated/>
#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<global::ConfigBase> _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<global::ConfigBase> 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<global::ConfigBase>(Create);
}

private static global::TUnit.Mocks.Mock<global::ConfigBase> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::ConfigBase>(behavior);
var impl = new ConfigBaseMockImpl(engine);
engine.Raisable = impl;
var mock = new global::TUnit.Mocks.Mock<global::ConfigBase>(impl, engine);
return mock;
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public static class ConfigBase_MockMemberExtensions
{
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks
{
public static class ConfigBase_MockStaticExtension
{
extension(global::ConfigBase _)
{
public static global::TUnit.Mocks.Mock<global::ConfigBase> Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
return global::TUnit.Mocks.Mock.Of<global::ConfigBase>(behavior);
}
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated;
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::MyService> engine, string name) : base(name)
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine<global::MyService> engine, int id) : base(id)
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine<global::MyService> engine, string host, int port) : base(host, port)
{
_engine = engine;
}
[global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
internal MyServiceMockImpl(global::TUnit.Mocks.MockEngine<global::MyService> engine, int timeout, bool verbose) : base(timeout, verbose)
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::MyService> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::BaseDialog> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::ExternalLib.ExternalClient> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::ExternalLib.ServiceClient> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::ExternalLib.ExternalResponse> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::BaseService> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::SelfEquatableSnapshot> engine) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::ExternalLib.ExternalService> engine, global::ExternalLib.ExternalService wrappedInstance) : base()
{
_engine = engine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::Repository> engine, global::Repository wrappedInstance) : base()
{
_engine = engine;
Expand Down
9 changes: 9 additions & 0 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;");
Expand All @@ -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;");
Expand All @@ -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;");
Expand Down Expand Up @@ -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;");
Expand All @@ -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;");
Expand All @@ -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;");
Expand Down
55 changes: 49 additions & 6 deletions TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,22 @@ public interface ICancellableStream
IAsyncEnumerable<int> 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
Expand Down Expand Up @@ -570,7 +581,39 @@ static async IAsyncEnumerable<int> 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.

Expand Down
Loading