Skip to content
Open
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
3 changes: 3 additions & 0 deletions Docs/pages/01-create-mocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ wrappedDispenser.Mock.Verify.Dispense(It.Is("Dark"), It.Is(5)).Once();

- Both interface and class types can be wrapped.
- All public calls are forwarded to the wrapped instance.
- Members that hide a base member with `new` are forwarded to the interface that declares them, so each
interface view of the mock reaches the matching member on the wrapped instance.
- You can still set up custom behavior that overrides the wrapped instance's behavior.
- Protected members are not forwarded to the wrapped instance; the base class implementation is used instead.
- Init-only properties are not forwarded to the wrapped instance, since it is already constructed.
- Verification works the same as with regular mocks.
40 changes: 24 additions & 16 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1592,8 +1592,12 @@ private static void AppendMockSubject_ImplementClass_AddEvent(StringBuilder sb,
}

sb.AppendLine("\t\t{");
bool supportsWrapping = @event is { IsStatic: false, IsProtected: false, ExplicitImplementation: null, } &&
bool supportsWrapping = @event is { IsStatic: false, IsProtected: false, } &&
!explicitInterfaceImplementation;
// An event that hides a base member (`new`) is emitted as an explicit interface implementation.
// The wrapped instance must be cast to the declaring interface, otherwise the subscription
// binds to the hiding member, whose delegate type differs from the one being implemented.
string wrapsType = @event.ExplicitImplementation ?? className;
bool supportsBaseForwarding = supportsWrapping && !isClassInterface && @event.UseOverride && !@event.IsAbstract;
if (supportsWrapping)
{
Expand All @@ -1604,7 +1608,7 @@ private static void AppendMockSubject_ImplementClass_AddEvent(StringBuilder sb,
sb.Append("\t\t\t\t\t").Append(mockRegistry).Append(addCall).Append(@event.GetUniqueNameString()).Append(", value.Target, value.Method);").AppendLine();
sb.Append("\t\t\t\t}").AppendLine();
sb.Append("\t\t\t\t").Append(backingFieldAccess).Append(" += value;").AppendLine();
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className).Append(" wraps)").AppendLine();
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType).Append(" wraps)").AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\twraps.").Append(@event.Name).Append(" += value;").AppendLine();
sb.Append("\t\t\t\t}").AppendLine();
Expand All @@ -1624,7 +1628,7 @@ private static void AppendMockSubject_ImplementClass_AddEvent(StringBuilder sb,
sb.Append("\t\t\t\t\t").Append(mockRegistry).Append(removeCall).Append(@event.GetUniqueNameString()).Append(", value.Target, value.Method);").AppendLine();
sb.Append("\t\t\t\t}").AppendLine();
sb.Append("\t\t\t\t").Append(backingFieldAccess).Append(" -= value;").AppendLine();
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className).Append(" wraps)").AppendLine();
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType).Append(" wraps)").AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\twraps.").Append(@event.Name).Append(" -= value;").AppendLine();
sb.Append("\t\t\t\t}").AppendLine();
Expand Down Expand Up @@ -1669,6 +1673,10 @@ private static void AppendMockSubject_ImplementClass_AddProperty(StringBuilder s
#pragma warning restore S107
{
string mockRegistry = property.IsStatic ? "MockRegistryProvider.Value" : $"this.{mockRegistryName}";
// A property that hides a base member (`new`) is emitted as an explicit interface implementation.
// The wrapped instance must be cast to the declaring interface, otherwise the delegated access
// binds to the hiding member instead: wrong property type (CS0266).
string wrapsType = property.ExplicitImplementation ?? className;
bool useFastForProperty = useFastBuffers && !property.IsIndexer && IsFastBufferEligibleProperty(property);
bool useFastForIndexer = useFastBuffers && property.IsIndexer && IsFastBufferEligibleIndexer(property);
string indexerGetIdRef = property.IsIndexer
Expand Down Expand Up @@ -1764,7 +1772,7 @@ property.IndexerParameters is not null
{
AppendRefStructIndexerGetterBody(sb, property, mockRegistry);
}
else if (isClassInterface && !explicitInterfaceImplementation && property.ExplicitImplementation is null)
else if (isClassInterface && !explicitInterfaceImplementation)
{
if (property is { IsIndexer: true, IndexerParameters: not null, })
{
Expand All @@ -1781,7 +1789,7 @@ property.IndexerParameters is not null
property.Type, property.IndexerParameters.Value, useFastForIndexer,
useFastForIndexer ? indexerGetIdRef : null,
cachedBufferRef: indexerGetCachedBufferRef);
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is not ").Append(className)
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is not ").Append(wrapsType)
.Append(' ').Append(wrapsVarName).Append(')').AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\treturn ").Append(setupVarName).Append(" is null")
Expand Down Expand Up @@ -1814,7 +1822,7 @@ property.IndexerParameters is not null
.AppendDefaultValueGeneratorFor(property.Type, "b.DefaultValue");
if (!property.IsStatic)
{
sb.Append(", ").Append(mockRegistry).Append(".Wraps is not ").Append(className)
sb.Append(", ").Append(mockRegistry).Append(".Wraps is not ").Append(wrapsType)
.Append(" wraps ? null : () => wraps.").Append(property.Name);
}

Expand All @@ -1828,7 +1836,7 @@ property.IndexerParameters is not null
.AppendDefaultValueGeneratorFor(property.Type, $"{mockRegistry}.Behavior.DefaultValue");
if (!property.IsStatic)
{
sb.Append(", ").Append(mockRegistry).Append(".Wraps is not ").Append(className)
sb.Append(", ").Append(mockRegistry).Append(".Wraps is not ").Append(wrapsType)
.Append(" wraps ? null : () => wraps.").Append(property.Name);
}
else
Expand Down Expand Up @@ -1865,7 +1873,7 @@ property.IndexerParameters is not null
sb.Append("\t\t\t\t\t").AppendTypeOrWrapper(property.Type).Append(' ')
.Append(baseResultVarName).Append(" = this.")
.Append(mockRegistryName)
.Append(".Wraps is ").Append(className).Append(' ').Append(wrapsVarName).Append(" ? ")
.Append(".Wraps is ").Append(wrapsType).Append(' ').Append(wrapsVarName).Append(" ? ")
.Append(wrapsVarName).Append('[')
.Append(FormatIndexerParametersAsNames(property.IndexerParameters.Value))
.Append("] : base[")
Expand Down Expand Up @@ -1905,7 +1913,7 @@ property.IndexerParameters is not null
.AppendDefaultValueGeneratorFor(property.Type, "b.DefaultValue");
if (property is { IsStatic: false, } && property.Getter?.IsProtected != true)
{
sb.Append(", ").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append(", ").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(" wraps ? () => wraps.").Append(property.Name).Append(" : () => base.")
.Append(property.Name);
}
Expand All @@ -1924,7 +1932,7 @@ property.IndexerParameters is not null
.AppendDefaultValueGeneratorFor(property.Type, $"{mockRegistry}.Behavior.DefaultValue");
if (property is { IsStatic: false, } && property.Getter?.IsProtected != true)
{
sb.Append(", ").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append(", ").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(" wraps ? () => wraps.").Append(property.Name).Append(" : () => base.")
.Append(property.Name);
}
Expand Down Expand Up @@ -2001,7 +2009,7 @@ property.IndexerParameters is not null
{
AppendRefStructIndexerSetterBody(sb, property, mockRegistry);
}
else if (isClassInterface && !explicitInterfaceImplementation && property.ExplicitImplementation is null)
else if (isClassInterface && !explicitInterfaceImplementation)
{
if (property is { IsIndexer: true, IndexerParameters: not null, })
{
Expand All @@ -2022,7 +2030,7 @@ property.IndexerParameters is not null
.Append(signatureIndex).Append(");")
.AppendLine();

sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(' ').Append(wrapsVarName).Append(')').AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\t").Append(wrapsVarName).Append('[')
Expand Down Expand Up @@ -2050,7 +2058,7 @@ property.IndexerParameters is not null

if (!property.IsStatic && !property.Setter.IsInitOnly)
{
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(" wraps)").AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\twraps.").Append(property.Name).Append(" = value;").AppendLine();
Expand Down Expand Up @@ -2080,7 +2088,7 @@ property.IndexerParameters is not null
sb.Append("\t\t\t\t{").AppendLine();
if (property.Setter?.IsProtected != true)
{
sb.Append("\t\t\t\t\tif (this.").Append(mockRegistryName).Append(".Wraps is ").Append(className)
sb.Append("\t\t\t\t\tif (this.").Append(mockRegistryName).Append(".Wraps is ").Append(wrapsType)
.Append(' ').Append(wrapsVarName).Append(')').AppendLine();
sb.Append("\t\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\t\t").Append(wrapsVarName).Append('[')
Expand Down Expand Up @@ -2136,9 +2144,9 @@ property.IndexerParameters is not null
}

sb.Append("\t\t\t\t{").AppendLine();
if (property is { IsStatic: false, } && property.Setter?.IsProtected != true)
if (property is { IsStatic: false, Setter: { IsProtected: false, IsInitOnly: false, }, })
{
sb.Append("\t\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append("\t\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(" wraps)").AppendLine();
sb.Append("\t\t\t\t\t{").AppendLine();
sb.Append("\t\t\t\t\t\twraps.").Append(property.Name).Append(" = value;").AppendLine();
Expand Down
37 changes: 37 additions & 0 deletions Tests/Mockolate.ExampleTests/ExampleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,41 @@ public async Task WithOut_ShouldSupportOutParameter(bool returnValue)
await That(result).IsEqualTo(returnValue);
sut.Mock.Verify.TryDelete(It.Is(id), It.IsOut<User?>()).Once();
}

[Fact]
public async Task Wrapping_HiddenMember_ShouldForwardToDeclaringInterface()
{
User alice = new(Guid.NewGuid(), "Alice");
User bob = new(Guid.NewGuid(), "Bob");
MyUserCache realCache = new();
IUserCache sut = IUserCache.CreateMock().Wrapping(realCache);

sut.Users = [alice,];
((IReadOnlyUserCache)sut).Users = [bob,];

// Each interface sees its own member on the wrapped instance.
await That(sut.Users).IsEqualTo([alice,]);
await That(((IReadOnlyUserCache)sut).Users).IsEqualTo([bob,]);
Comment on lines +279 to +284
await That(realCache.CacheUsers).IsEqualTo([alice,]);
await That(realCache.ReadOnlyUsers).IsEqualTo([bob,]);
}

private sealed class MyUserCache : IUserCache
{
public IList<User> CacheUsers { get; private set; } = [];

public IEnumerable<User> ReadOnlyUsers { get; private set; } = [];

public IList<User> Users
{
get => CacheUsers;
set => CacheUsers = value;
}

IEnumerable<User> IReadOnlyUserCache.Users
{
get => ReadOnlyUsers;
set => ReadOnlyUsers = value;
}
}
}
13 changes: 13 additions & 0 deletions Tests/Mockolate.ExampleTests/TestData/IUserCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Mockolate.ExampleTests.TestData;

public interface IReadOnlyUserCache
{
IEnumerable<User> Users { get; set; }
}

public interface IUserCache : IReadOnlyUserCache
{
new IList<User> Users { get; set; }
}
Comment on lines +5 to +13
Loading
Loading