Skip to content

Commit

Permalink
Merge pull request #1636 from thomaslevesque/wrapping-equals
Browse files Browse the repository at this point in the history
Fix equality for wrapping fakes
  • Loading branch information
blairconrad committed Oct 2, 2019
2 parents f4ce986 + 74a3ef0 commit 1177af6
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 34 deletions.
28 changes: 7 additions & 21 deletions src/FakeItEasy/Core/FakeManager.ObjectMemberRule.cs
@@ -1,23 +1,17 @@
namespace FakeItEasy.Core
{
using System;
using System.Linq;
using System.Reflection;
using static ObjectMembers;

/// <content>Object member rule.</content>
public partial class FakeManager
{
private class ObjectMemberRule
: SharedFakeObjectCallRule
{
private static readonly MethodInfo EqualsMethod = typeof(object).GetMethod(nameof(object.Equals), new[] { typeof(object) });
private static readonly MethodInfo ToStringMethod = typeof(object).GetMethod(nameof(object.ToString), Type.EmptyTypes);
private static readonly MethodInfo GetHashCodeMethod = typeof(object).GetMethod(nameof(object.GetHashCode), Type.EmptyTypes);

public override bool IsApplicableTo(IFakeObjectCall fakeObjectCall) =>
IsSameMethod(fakeObjectCall.Method, EqualsMethod) ||
IsSameMethod(fakeObjectCall.Method, ToStringMethod) ||
IsSameMethod(fakeObjectCall.Method, GetHashCodeMethod);
fakeObjectCall.Method.IsSameMethodAs(EqualsMethod) ||
fakeObjectCall.Method.IsSameMethodAs(ToStringMethod) ||
fakeObjectCall.Method.IsSameMethodAs(GetHashCodeMethod);

public override void Apply(IInterceptedFakeObjectCall fakeObjectCall)
{
Expand All @@ -38,17 +32,9 @@ public override void Apply(IInterceptedFakeObjectCall fakeObjectCall)
}
}

private static bool IsSameMethod(MethodInfo first, MethodInfo second)
{
return first.DeclaringType == second.DeclaringType
&& first.MetadataToken == second.MetadataToken
&& first.Module == second.Module
&& first.GetGenericArguments().SequenceEqual(second.GetGenericArguments());
}

private static bool TryHandleGetHashCode(IInterceptedFakeObjectCall fakeObjectCall, FakeManager fakeManager)
{
if (!IsSameMethod(fakeObjectCall.Method, GetHashCodeMethod))
if (!fakeObjectCall.Method.IsSameMethodAs(GetHashCodeMethod))
{
return false;
}
Expand All @@ -60,7 +46,7 @@ private static bool TryHandleGetHashCode(IInterceptedFakeObjectCall fakeObjectCa

private static bool TryHandleToString(IInterceptedFakeObjectCall fakeObjectCall, FakeManager fakeManager)
{
if (!IsSameMethod(fakeObjectCall.Method, ToStringMethod))
if (!fakeObjectCall.Method.IsSameMethodAs(ToStringMethod))
{
return false;
}
Expand All @@ -72,7 +58,7 @@ private static bool TryHandleToString(IInterceptedFakeObjectCall fakeObjectCall,

private static bool TryHandleEquals(IInterceptedFakeObjectCall fakeObjectCall, FakeManager fakeManager)
{
if (!IsSameMethod(fakeObjectCall.Method, EqualsMethod))
if (!fakeObjectCall.Method.IsSameMethodAs(EqualsMethod))
{
return false;
}
Expand Down
10 changes: 1 addition & 9 deletions src/FakeItEasy/Core/MethodInfoManager.cs
Expand Up @@ -46,7 +46,7 @@ private static bool HasSameBaseMethod(MethodInfo first, MethodInfo second)
var baseOfFirst = GetBaseDefinition(first);
var baseOfSecond = GetBaseDefinition(second);

return IsSameMethod(baseOfFirst, baseOfSecond);
return baseOfFirst.IsSameMethodAs(baseOfSecond);
}

private static MethodInfo GetBaseDefinition(MethodInfo method)
Expand All @@ -59,14 +59,6 @@ private static MethodInfo GetBaseDefinition(MethodInfo method)
return method.GetBaseDefinition();
}

private static bool IsSameMethod(MethodInfo first, MethodInfo second)
{
return first.DeclaringType == second.DeclaringType
&& first.MetadataToken == second.MetadataToken
&& first.Module == second.Module
&& first.GetGenericArguments().SequenceEqual(second.GetGenericArguments());
}

private static MethodInfo FindMethodOnTypeThatWillBeInvokedByMethodInfo(Type type, FakeItEasy.Compatibility.MethodInfoWrapper methodWrapper)
{
var result =
Expand Down
32 changes: 29 additions & 3 deletions src/FakeItEasy/Core/WrappedObjectRule.cs
@@ -1,6 +1,7 @@
namespace FakeItEasy.Core
{
using System.Reflection;
using static ObjectMembers;

/// <summary>
/// A call rule that applies to any call and just delegates the
Expand Down Expand Up @@ -51,18 +52,43 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall)
Guard.AgainstNull(fakeObjectCall, nameof(fakeObjectCall));

var parameters = fakeObjectCall.Arguments.GetUnderlyingArgumentsArray();
object valueFromWrappedInstance;
object returnValue;
try
{
valueFromWrappedInstance = fakeObjectCall.Method.Invoke(this.wrappedObject, parameters);
if (fakeObjectCall.Method.IsSameMethodAs(EqualsMethod))
{
var arg = parameters[0];
if (ReferenceEquals(arg, fakeObjectCall.FakedObject))
{
// fake.Equals(fake) returns true
returnValue = true;
}
else if (ReferenceEquals(arg, this.wrappedObject))
{
// fake.Equals(wrappedObject) returns wrappedObject.Equals(fake)
// This will be false if Equals isn't overriden (reference equality)
// and true if Equals is overriden to implement value semantics.
// This approach has the benefit of keeping Equals symmetrical.
returnValue = this.wrappedObject.Equals(fakeObjectCall.FakedObject);
}
else
{
// fake.Equals(somethingElse) is delegated to the wrapped object (no special case)
returnValue = this.wrappedObject.Equals(arg);
}
}
else
{
returnValue = fakeObjectCall.Method.Invoke(this.wrappedObject, parameters);
}
}
catch (TargetInvocationException ex)
{
ex.InnerException?.Rethrow();
throw;
}

fakeObjectCall.SetReturnValue(valueFromWrappedInstance);
fakeObjectCall.SetReturnValue(returnValue);
}
}
}
8 changes: 8 additions & 0 deletions src/FakeItEasy/MethodBaseExtensions.cs
Expand Up @@ -56,6 +56,14 @@ public static string GetDescription(this MethodBase method)
return builder.ToString();
}

public static bool IsSameMethodAs(this MethodBase method, MethodBase otherMethod)
{
return method.DeclaringType == otherMethod.DeclaringType
&& method.MetadataToken == otherMethod.MetadataToken
&& method.Module == otherMethod.Module
&& method.GetGenericArguments().SequenceEqual(otherMethod.GetGenericArguments());
}

private static void AppendMethodName(StringBuilder builder, MethodBase method)
{
if (IsPropertyGetterOrSetter(method))
Expand Down
12 changes: 12 additions & 0 deletions src/FakeItEasy/ObjectMembers.cs
@@ -0,0 +1,12 @@
namespace FakeItEasy
{
using System;
using System.Reflection;

internal static class ObjectMembers
{
public static readonly MethodInfo EqualsMethod = typeof(object).GetMethod(nameof(object.Equals), new[] { typeof(object) });
public static readonly MethodInfo ToStringMethod = typeof(object).GetMethod(nameof(object.ToString), Type.EmptyTypes);
public static readonly MethodInfo GetHashCodeMethod = typeof(object).GetMethod(nameof(object.GetHashCode), Type.EmptyTypes);
}
}
91 changes: 90 additions & 1 deletion tests/FakeItEasy.Specs/WrappingFakeSpecs.cs
@@ -1,4 +1,4 @@
namespace FakeItEasy.Specs
namespace FakeItEasy.Specs
{
using System;
using FakeItEasy.Tests.TestHelpers;
Expand All @@ -17,6 +17,11 @@ public interface IFoo
void OutAndRefMethod(ref int @ref, out int @out);
}

public interface IBar
{
int Id { get; }
}

[Scenario]
public static void NonVoidSuccess(
Foo realObject,
Expand Down Expand Up @@ -136,6 +141,70 @@ public interface IFoo
.x(() => @out.Should().Be(42));
}

[Scenario]
public static void FakeEqualsFake(Foo realObject, IFoo wrapper, bool equals)
{
"Given a real object"
.x(() => realObject = new Foo());

"And a fake wrapping this object"
.x(() => wrapper = A.Fake<IFoo>(o => o.Wrapping(realObject)));

"When Equals is called on the fake with itself as the argument"
.x(() => equals = wrapper.Equals(wrapper));

"Then it should return true"
.x(() => equals.Should().BeTrue());
}

[Scenario]
public static void FakeEqualsWrappedObject(Foo realObject, IFoo wrapper, bool equals)
{
"Given a real object"
.x(() => realObject = new Foo());

"And a fake wrapping this object"
.x(() => wrapper = A.Fake<IFoo>(o => o.Wrapping(realObject)));

"When Equals is called on the fake with the real object as the argument"
.x(() => equals = wrapper.Equals(realObject));

"Then it should return false"
.x(() => equals.Should().BeFalse());
}

[Scenario]
public static void FakeEqualsFakeWithValueSemantics(Bar realObject, IBar wrapper, bool equals)
{
"Given a real object that overrides Equals with value semantics"
.x(() => realObject = new Bar(42));

"And a fake wrapping this object"
.x(() => wrapper = A.Fake<IBar>(o => o.Wrapping(realObject)));

"When Equals is called on the fake with itself as the argument"
.x(() => equals = wrapper.Equals(wrapper));

"Then it should return true"
.x(() => equals.Should().BeTrue());
}

[Scenario]
public static void FakeEqualsWrappedObjectWithValueSemantics(Bar realObject, IBar wrapper, bool equals)
{
"Given a real object that overrides Equals with value semantics"
.x(() => realObject = new Bar(42));

"And a fake wrapping this object"
.x(() => wrapper = A.Fake<IBar>(o => o.Wrapping(realObject)));

"When Equals is called on the fake with the real object as the argument"
.x(() => equals = wrapper.Equals(realObject));

"Then it should return true"
.x(() => equals.Should().BeTrue());
}

public class Foo : IFoo
{
public bool NonVoidMethodCalled { get; private set; }
Expand Down Expand Up @@ -171,5 +240,25 @@ public void OutAndRefMethod(ref int @ref, out int @out)
@out = 42;
}
}

public class Bar : IBar
{
public Bar(int id)
{
this.Id = id;
}

public int Id { get; }

public override bool Equals(object obj)
{
return obj is IBar other && other.Id == this.Id;
}

public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
}
}

0 comments on commit 1177af6

Please sign in to comment.