Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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
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 @@ -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
10 changes: 10 additions & 0 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -139,6 +143,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 @@ -155,6 +160,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 @@ -169,6 +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(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 @@ -478,6 +485,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 @@ -494,6 +502,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 @@ -508,6 +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(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 @@ -180,11 +180,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 @@ -462,7 +473,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