Working towards proper integration of elevated mocks with nsub (still much to do)
This commit is contained in:
Родитель
404d9c6e55
Коммит
4760208090
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Exceptions;
|
||||
using NSubstitute.Proxies;
|
||||
using NSubstitute.Proxies.CastleDynamicProxy;
|
||||
using NSubstitute.Proxies.DelegateProxy;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
class ElevatedProxyFactory : IProxyFactory
|
||||
{
|
||||
readonly ElevatedProxyMapper m_ElevatedProxyMapper;
|
||||
readonly IProxyFactory m_DefaultProxyFactory = new ProxyFactory(new DelegateProxyFactory(), new CastleDynamicProxyFactory());
|
||||
|
||||
public ElevatedProxyFactory(ElevatedProxyMapper elevatedProxyMapper) => m_ElevatedProxyMapper = elevatedProxyMapper;
|
||||
|
||||
object IProxyFactory.GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
|
||||
{
|
||||
if (!ShouldHandle(typeToProxy))
|
||||
return m_DefaultProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
|
||||
|
||||
if (typeToProxy == typeof(SubstituteStatic.Proxy))
|
||||
{
|
||||
if (additionalInterfaces != null && additionalInterfaces.Any())
|
||||
throw new SubstituteException("Can not substitute interfaces as static");
|
||||
|
||||
var actualType = (Type)constructorArguments[0];
|
||||
|
||||
return m_ElevatedProxyMapper.MockStatic(actualType, callRouter);
|
||||
}
|
||||
|
||||
throw NotImplementedException();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool ShouldHandle(Type typeToProxy)
|
||||
{
|
||||
if (typeToProxy.IsInterface || typeToProxy.IsAbstract)
|
||||
return false;
|
||||
|
||||
// TEMP
|
||||
if (typeToProxy.FullName != "SystemUnderTest.SimpleClass")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Elevated.Utilities;
|
||||
using NSubstitute.Exceptions;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
class ElevatedProxyMapper
|
||||
{
|
||||
readonly ISubstitutionContext m_SubstitutionContext;
|
||||
readonly CallFactory m_CallFactory;
|
||||
|
||||
public ElevatedProxyMapper(ISubstitutionContext substitutionContext)
|
||||
{
|
||||
m_SubstitutionContext = substitutionContext;
|
||||
m_CallFactory = new CallFactory(m_SubstitutionContext);
|
||||
}
|
||||
|
||||
public SubstituteStatic.Proxy MockStatic(Type type, ICallRouter callRouter)
|
||||
{
|
||||
var staticField = GetStaticFieldInfo(type);
|
||||
if (staticField == null)
|
||||
throw new SubstituteException("Can not substitute for non-patched types");
|
||||
if (staticField.GetValue(null) != null)
|
||||
throw new SubstituteException("Can not substitute the same type twice (did you forget to Dispose() your previous substitute?)");
|
||||
|
||||
staticField.SetValue(null, callRouter);
|
||||
|
||||
return new SubstituteStatic.Proxy(new DelegateDisposable(() =>
|
||||
{
|
||||
var found = staticField.GetValue(null);
|
||||
if (found == null)
|
||||
throw new SubstituteException("Unexpected static unmock of an already unmocked type");
|
||||
if (found != callRouter)
|
||||
throw new SubstituteException("Discovered unexpected call router attached in static mock context");
|
||||
|
||||
staticField.SetValue(null, null);
|
||||
}));
|
||||
}
|
||||
|
||||
public void Mock(Type type, object instance, ICallRouter callRouter)
|
||||
{
|
||||
var field = GetFieldInfo(type);
|
||||
if (field == null)
|
||||
throw new SubstituteException("Can not substitute for non-patched types");
|
||||
|
||||
field.SetValue(instance, callRouter);
|
||||
}
|
||||
|
||||
public bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, MethodInfo method, Type[] methodGenericTypes, object[] args)
|
||||
{
|
||||
var field = instance == null ? GetStaticFieldInfo(actualType) : GetFieldInfo(actualType);
|
||||
var callRouter = (ICallRouter)field?.GetValue(instance);
|
||||
|
||||
if (callRouter != null)
|
||||
{
|
||||
Func<object> baseResult = () => invocation.Proceed(); // $$$ need to turn this into a func reentry which goes straight to the leftover
|
||||
var result = new Lazy<object>(baseResult);
|
||||
Func<object> baseMethod = () => result.Value;
|
||||
|
||||
var mappedInvocation = m_CallFactory.Create(method, args, instance, baseMethod);
|
||||
Array.Copy(mappedInvocation.GetArguments(), args, args.Length); // $$$ unsure about this..apparently need to copy back results, but our version has a bug on this
|
||||
mockedReturnValue = callRouter.Route(mappedInvocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
mockedReturnValue = mockedReturnType.GetDefaultValue();
|
||||
return false;
|
||||
}
|
||||
|
||||
FieldInfo GetStaticFieldInfo(Type type) => m_StaticFieldCache.GetOrAdd(type, t => t.GetField("__mockStaticRouter", BindingFlags.Static | BindingFlags.NonPublic));
|
||||
FieldInfo GetFieldInfo(Type type) => m_FieldCache.GetOrAdd(type, t => t.GetField("__mockRouter", BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
|
||||
readonly Dictionary<Type, FieldInfo> m_StaticFieldCache = new Dictionary<Type, FieldInfo>();
|
||||
readonly Dictionary<Type, FieldInfo> m_FieldCache = new Dictionary<Type, FieldInfo>();
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
using System;
|
||||
using NSubstitute.Core;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
class ElevatedSubstituteFactory : ISubstituteFactory
|
||||
{
|
||||
readonly ISubstituteFactory m_Forwarder;
|
||||
|
||||
public ElevatedSubstituteFactory(ISubstituteFactory forwarder)
|
||||
=> m_Forwarder = forwarder;
|
||||
|
||||
object ISubstituteFactory.Create(Type[] typesToProxy, object[] constructorArguments)
|
||||
=> m_Forwarder.Create(typesToProxy, constructorArguments);
|
||||
|
||||
object ISubstituteFactory.CreatePartial(Type[] typesToProxy, object[] constructorArguments)
|
||||
=> m_Forwarder.CreatePartial(typesToProxy, constructorArguments);
|
||||
|
||||
ICallRouter ISubstituteFactory.GetCallRouterCreatedFor(object substitute)
|
||||
=> m_Forwarder.GetCallRouterCreatedFor(substitute);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,13 @@ using System.Collections.Generic;
|
|||
using JetBrains.Annotations;
|
||||
using NSubstitute.Core;
|
||||
using NSubstitute.Core.Arguments;
|
||||
using NSubstitute.Elevated.Utilities;
|
||||
using NSubstitute.Exceptions;
|
||||
using NSubstitute.Routing;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
// this class exists solely to hook in our own proxy factory to the nsub machinery
|
||||
public class ElevatedSubstitutionContext : ISubstitutionContext
|
||||
{
|
||||
readonly ISubstitutionContext m_Forwarder;
|
||||
|
@ -15,9 +18,26 @@ namespace NSubstitute.Elevated
|
|||
public ElevatedSubstitutionContext([NotNull] ISubstitutionContext forwarder)
|
||||
{
|
||||
m_Forwarder = forwarder;
|
||||
m_ElevatedSubstituteFactory = new ElevatedSubstituteFactory(forwarder.SubstituteFactory);
|
||||
m_ElevatedSubstituteFactory = new SubstituteFactory(this,
|
||||
new CallRouterFactory(), new ElevatedProxyFactory(ElevatedProxyMapper), new CallRouterResolver());
|
||||
}
|
||||
|
||||
public static IDisposable AutoHook()
|
||||
{
|
||||
var hookedContext = SubstitutionContext.Current;
|
||||
var thisContext = new ElevatedSubstitutionContext(hookedContext);
|
||||
SubstitutionContext.Current = thisContext;
|
||||
|
||||
return new DelegateDisposable(() =>
|
||||
{
|
||||
if (SubstitutionContext.Current != thisContext)
|
||||
throw new SubstituteException("Unexpected hook in place of ours");
|
||||
SubstitutionContext.Current = hookedContext;
|
||||
});
|
||||
}
|
||||
|
||||
internal ElevatedProxyMapper ElevatedProxyMapper { get; } = new ElevatedProxyMapper();
|
||||
|
||||
// this is the only one we're overriding for now, so we can hook our own factory in there.
|
||||
ISubstituteFactory ISubstitutionContext.SubstituteFactory => m_ElevatedSubstituteFactory;
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> @this, TKey key, Func<TKey, TValue> createFunc)
|
||||
{
|
||||
if (@this.TryGetValue(key, out var found))
|
||||
return found;
|
||||
|
||||
found = createFunc(key);
|
||||
@this.Add(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
public static object GetDefaultValue(this Type @this)
|
||||
{
|
||||
object defaultValue = null;
|
||||
if (@this.IsValueType && @this != typeof(void))
|
||||
defaultValue = Activator.CreateInstance(@this);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NSubstitute.Core;
|
||||
|
||||
// this namespace contains types that must be public in order to be usable from patched assemblies, yet
|
||||
// we do not want used from normal client api
|
||||
namespace NSubstitute.Elevated.WeaverInternals
|
||||
{
|
||||
// important: keep all non-mscorlib types out of the public surface area of this class, so as to
|
||||
// avoid needing to add more references than NSubstitute.Elevated to the assembly during patching.
|
||||
|
||||
public static class PatchedAssemblyBridge
|
||||
{
|
||||
// returns true if a mock is in place and it is taking over functionality. instance may be null
|
||||
// if static. mockedReturnValue is ignored in a void return func.
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, Type[] methodGenericTypes, object[] args)
|
||||
{
|
||||
if (!(SubstitutionContext.Current is ElevatedSubstitutionContext elevated))
|
||||
{
|
||||
mockedReturnValue = mockedReturnType.GetDefaultValue();
|
||||
return false;
|
||||
}
|
||||
|
||||
var method = (MethodInfo) new StackTrace(1).GetFrame(0).GetMethod();
|
||||
|
||||
if (method.IsGenericMethodDefinition)
|
||||
method = method.MakeGenericMethod(methodGenericTypes);
|
||||
|
||||
return elevated.ElevatedProxyMapper.TryMock(actualType, instance, mockedReturnType, out mockedReturnValue, method, methodGenericTypes, args);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace NSubstitute.Elevated
|
||||
{
|
||||
public static class SubstituteStatic
|
||||
{
|
||||
// callers need an actual object in order to chain further arranging, so we return this placeholder for static substitutes
|
||||
public class Proxy : IDisposable
|
||||
{
|
||||
readonly IDisposable m_Forwarder;
|
||||
|
||||
internal Proxy(IDisposable forwarder) => m_Forwarder = forwarder;
|
||||
public void Dispose() { m_Forwarder.Dispose(); }
|
||||
}
|
||||
|
||||
public static Proxy For<T>() => For(typeof(T));
|
||||
public static Proxy For(Type staticType) => Substitute.For<Proxy>(staticType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace NSubstitute.Elevated.Utilities
|
||||
{
|
||||
public class DelegateDisposable : IDisposable
|
||||
{
|
||||
readonly Action m_DisposeAction;
|
||||
|
||||
public DelegateDisposable([NotNull] Action disposeAction) => m_DisposeAction = disposeAction;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_DisposeAction();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using SystemUnderTest;
|
||||
using NSubstitute;
|
||||
using NSubstitute.Exceptions;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
@ -10,6 +9,20 @@ namespace NSubstitute.Elevated.Tests
|
|||
[TestFixture]
|
||||
class BasicTests
|
||||
{
|
||||
IDisposable m_Dispose;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
m_Dispose = ElevatedSubstitutionContext.AutoHook();
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
m_Dispose.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MockByInterface_ShouldUseNSubDefaultBehavior()
|
||||
{
|
||||
|
@ -46,7 +59,7 @@ namespace NSubstitute.Elevated.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void ClassWithNoDefaultCtor_MocksWithoutError()
|
||||
public void ClassWithNoDefaultCtor_Mocks()
|
||||
{
|
||||
var sub = Substitute.For<ClassWithNoDefaultCtor>();
|
||||
|
||||
|
@ -55,7 +68,7 @@ namespace NSubstitute.Elevated.Tests
|
|||
|
||||
# if TEST_ICALLS
|
||||
[Test]
|
||||
public void ClassWithICallInCtor_MocksWithoutError()
|
||||
public void ClassWithICallInCtor_Mocks()
|
||||
{
|
||||
// $ TODO: make this into an actual test of the icall thing. currently just checks that doesn't throw..not that interesting
|
||||
|
||||
|
@ -67,7 +80,7 @@ namespace NSubstitute.Elevated.Tests
|
|||
# endif
|
||||
|
||||
[Test]
|
||||
public void ClassWithThrowInCtor_MocksWithoutError()
|
||||
public void ClassWithThrowInCtor_Mocks()
|
||||
{
|
||||
var sub = Substitute.For<ClassWithCtorThrow>();
|
||||
|
||||
|
@ -102,14 +115,14 @@ namespace NSubstitute.Elevated.Tests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void NonMockedClassWithDependentTypes_LoadsWithoutError()
|
||||
public void NonMockedClassWithDependentTypes_Loads()
|
||||
{
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
typeof(ClassWithDependency).GetMethod("Dummy").ReturnType.FullName.ShouldBe("mycodedep.DependentType");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClassWithDependentTypes_MocksWithoutError()
|
||||
public void ClassWithDependentTypes_Mocks()
|
||||
{
|
||||
// simple test to ensure that we can patch methods that use types from foreign assemblies
|
||||
|
||||
|
@ -118,5 +131,37 @@ namespace NSubstitute.Elevated.Tests
|
|||
// ReSharper disable once PossibleNullReferenceException
|
||||
sub.GetType().GetMethod("Dummy").ReturnType.FullName.ShouldBe("mycodedep.DependentType");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimpleClass_FullMock_DoesNotCallDefaultImpls()
|
||||
{
|
||||
var sub = Substitute.For<SimpleClass>();
|
||||
|
||||
sub.VoidMethod(5);
|
||||
sub.Modified.ShouldBe(0);
|
||||
|
||||
sub.ReturnMethod(5).ShouldBe(0);
|
||||
sub.Modified.ShouldBe(0);
|
||||
|
||||
sub.ReturnMethod(5).Returns(10);
|
||||
sub.ReturnMethod(5).ShouldBe(10);
|
||||
sub.Modified.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimpleClass_PartialMock_CallsDefaultImpls()
|
||||
{
|
||||
var sub = Substitute.For<SimpleClass>();
|
||||
|
||||
sub.VoidMethod(5);
|
||||
sub.Modified.ShouldBe(5);
|
||||
|
||||
sub.ReturnMethod(3).ShouldBe(8);
|
||||
sub.Modified.ShouldBe(8);
|
||||
|
||||
sub.ReturnMethod(4).Returns(10); // $$$ whats the right way to do this without triggering the method call?
|
||||
sub.ReturnMethod(4).ShouldBe(10);
|
||||
sub.Modified.ShouldBe(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
using System;
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
public static class MockWeaverTestUtils
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace NSubstitute.Elevated.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class MockWeaverTests
|
||||
{
|
||||
[NonSerialized]
|
||||
object __mockContext;
|
||||
[NonSerialized]
|
||||
static object __mockStaticContext;
|
||||
|
||||
[Test]
|
||||
public void NonParamStaticMethod()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@
|
|||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>11.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mono.Cecil">
|
||||
<Version>0.9.6.4</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute">
|
||||
<Version>2.0.3</Version>
|
||||
</PackageReference>
|
||||
|
@ -22,6 +25,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\source\NSubstitute.Elevated\NSubstitute.Elevated.csproj" />
|
||||
<ProjectReference Include="..\Support\SystemUnderTest\SystemUnderTest.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using NSubstitute.Elevated.WeaverInternals;
|
||||
|
||||
#if TEST_ICALLS
|
||||
using System.Runtime.CompilerServices;
|
||||
|
@ -77,4 +78,50 @@ namespace SystemUnderTest
|
|||
{
|
||||
public DependentAssembly.DependentType Dummy => new DependentAssembly.DependentType();
|
||||
}
|
||||
|
||||
public class SimpleClass
|
||||
{
|
||||
public int Modified;
|
||||
|
||||
// actual
|
||||
//public void VoidMethod() => ++Modified;
|
||||
//public int ReturnMethod() => ++Modified;
|
||||
|
||||
// hack until patching works
|
||||
|
||||
public void VoidMethod(int count)
|
||||
{
|
||||
if (PatchedAssemblyBridge.TryMock(this, new object[] { count }))
|
||||
return;
|
||||
|
||||
Modified += count;
|
||||
}
|
||||
|
||||
public int ReturnMethod(int count)
|
||||
{
|
||||
if (PatchedAssemblyBridge.TryMock(out var returnValue, this, new object[] { count }))
|
||||
return (int)returnValue;
|
||||
|
||||
return Modified += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace NSubstitute.Elevated.WeaverInternals
|
||||
{
|
||||
public static class PatchedAssemblyBridge
|
||||
{
|
||||
public static bool TryMock(object instance, object[] methodCallArgs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryMock(out object returnValue, object instance, object[] methodCallArgs)
|
||||
{
|
||||
returnValue = null;
|
||||
// $$$ use https://stackoverflow.com/a/353073 when figure out what return value type is
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче