diff --git a/src/DivertR/ISpy.cs b/src/DivertR/ISpy.cs index dedc569c..62aab9b4 100644 --- a/src/DivertR/ISpy.cs +++ b/src/DivertR/ISpy.cs @@ -11,7 +11,7 @@ namespace DivertR /// /// inherits from and therefore its Mock behaviour can be configured using the same Redirect fluent interface e.g. to add one or more instances or reset. /// The Spy is preconfigured with a Via that records the mock object calls readable from the property. - /// As with a normal Redirect, Spy reset removes all Vias, however it also adds a new Mock call record Via and the property is replaced. + /// Spy.Reset behaves the sames as the base Redirect class and removes all Vias except a new call record Via is then added back. Reset also sets a new property. /// public interface ISpy : IRedirect { @@ -26,7 +26,7 @@ public interface ISpy : IRedirect IRecordStream Calls { get; } /// - /// Insert a into this Spy. + /// Insert an into this Spy. /// /// The Via instance to insert. /// Optional action. diff --git a/src/DivertR/Internal/RedirectTracker.cs b/src/DivertR/Internal/RedirectTracker.cs new file mode 100644 index 00000000..f724e862 --- /dev/null +++ b/src/DivertR/Internal/RedirectTracker.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace DivertR.Internal +{ + internal class RedirectTracker + { + private readonly ConditionalWeakTable _redirectTable = new(); + + public void AddRedirect(Redirect redirect, [DisallowNull] TTarget proxy) where TTarget : class? + { + _redirectTable.Add(proxy, redirect); + } + + public Redirect GetRedirect([DisallowNull] TTarget proxy) where TTarget : class? + { + if (!_redirectTable.TryGetValue(proxy, out var redirect)) + { + throw new DiverterException("Redirect not found"); + } + + if (redirect is not Redirect redirectOf) + { + throw new DiverterException($"Redirect target type: {redirect.RedirectId.Type} does not match proxy type: {typeof(TTarget)}"); + } + + return redirectOf; + } + } +} \ No newline at end of file diff --git a/src/DivertR/Internal/SpyTracker.cs b/src/DivertR/Internal/SpyTracker.cs deleted file mode 100644 index 0463255f..00000000 --- a/src/DivertR/Internal/SpyTracker.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace DivertR.Internal -{ - internal class SpyTracker - { - private readonly ConditionalWeakTable _spyTable = new(); - - public void AddSpy(Spy spy, [DisallowNull] TTarget mock) where TTarget : class? - { - _spyTable.Add(mock, spy); - } - - public Spy GetSpy([DisallowNull] TTarget mock) where TTarget : class? - { - if (!_spyTable.TryGetValue(mock, out var spy)) - { - throw new DiverterException("Spy not found"); - } - - if (spy is not Spy spyOf) - { - throw new DiverterException($"Spy target type: {spy.RedirectId.Type} does not match mock type: {typeof(TTarget)}"); - } - - return spyOf; - } - } -} \ No newline at end of file diff --git a/src/DivertR/Redirect.cs b/src/DivertR/Redirect.cs index 9cf4e9ef..8ba71a48 100644 --- a/src/DivertR/Redirect.cs +++ b/src/DivertR/Redirect.cs @@ -114,10 +114,19 @@ public TTarget Proxy(TTarget? root) { if (root is null || !RedirectSet.Settings.CacheRedirectProxies) { - return _proxyFactory.CreateProxy(_redirectProxyCall, root); + var createdProxy = _proxyFactory.CreateProxy(_redirectProxyCall, root); + Redirect.Track(this, createdProxy); + + return createdProxy; } - var proxy = _proxyCache.GetValue(root, x => _proxyFactory.CreateProxy(_redirectProxyCall, x)); + var proxy = _proxyCache.GetValue(root, x => + { + var createdProxy = _proxyFactory.CreateProxy(_redirectProxyCall, x); + Redirect.Track(this, createdProxy); + + return createdProxy; + }); return proxy!; } @@ -250,4 +259,53 @@ protected virtual void ResetInternal() RedirectRepository.Reset(); } } + + /// + /// Redirect helper class for creating proxy objects directly. + /// + public static class Redirect + { + private static readonly RedirectTracker RedirectTracker = new(); + + /// + /// Creates and returns a proxy of the given target type. + /// + /// The proxy target type. + /// The created Redirect proxy object. + public static TTarget Proxy() where TTarget : class? + { + var redirect = new Redirect(); + + return redirect.Proxy(); + } + + /// + /// Creates and returns a proxy of the given target type. + /// + /// /// The proxy target type. + /// The root instance the proxy will wrap and relay calls to. + /// The created Redirect proxy object. + public static TTarget Proxy(TTarget? root) where TTarget : class? + { + var redirect = new Redirect(); + + return redirect.Proxy(root); + } + + /// + /// Gets the of the proxy object. + /// + /// The Redirect proxy object. + /// The Redirect instance. + /// Thrown if if the given object does not have an associated + public static IRedirect Of([DisallowNull] TTarget proxy) where TTarget : class? + { + return RedirectTracker.GetRedirect(proxy); + } + + internal static void Track(Redirect redirect, [DisallowNull] TTarget proxy) where TTarget : class? + { + RedirectTracker.AddRedirect(redirect, proxy); + } + } } diff --git a/src/DivertR/RedirectId.cs b/src/DivertR/RedirectId.cs index df4f251e..8394758b 100644 --- a/src/DivertR/RedirectId.cs +++ b/src/DivertR/RedirectId.cs @@ -34,7 +34,7 @@ public bool Equals(RedirectId other) return _id.Equals(other._id); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is RedirectId other && Equals(other); } diff --git a/src/DivertR/Spy.cs b/src/DivertR/Spy.cs index dbf300f9..5a51f33a 100644 --- a/src/DivertR/Spy.cs +++ b/src/DivertR/Spy.cs @@ -25,7 +25,6 @@ private Spy(DiverterSettings? diverterSettings, TTarget? root, bool hasRoot) : base(diverterSettings) { Mock = hasRoot ? Proxy() : Proxy(root); - Spy.AddSpy(this, Mock); ResetAndConfigureRecord(); } @@ -134,8 +133,6 @@ private RecordStream CallsLocked /// public static class Spy { - private static readonly SpyTracker SpyTracker = new(); - /// /// Creates a spy of the given target type and returns its mock object. /// @@ -167,12 +164,14 @@ public static TTarget On(TTarget? root) where TTarget : class? /// Thrown if if the given object does not have an associated public static ISpy Of([DisallowNull] TTarget mock) where TTarget : class? { - return SpyTracker.GetSpy(mock); - } - - internal static void AddSpy(Spy spy, [DisallowNull] TTarget mock) where TTarget : class? - { - SpyTracker.AddSpy(spy, mock); + var redirect = Redirect.Of(mock); + + if (redirect is not Spy spyOf) + { + throw new DiverterException("Spy not found"); + } + + return spyOf; } } } \ No newline at end of file diff --git a/test/DivertR.UnitTests/QuickstartExamples.cs b/test/DivertR.UnitTests/QuickstartExamples.cs index 394d83d8..9146daa3 100644 --- a/test/DivertR.UnitTests/QuickstartExamples.cs +++ b/test/DivertR.UnitTests/QuickstartExamples.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading.Tasks; using DivertR.DependencyInjection; using DivertR.UnitTests.Model; diff --git a/test/DivertR.UnitTests/RedirectStaticTests.cs b/test/DivertR.UnitTests/RedirectStaticTests.cs new file mode 100644 index 00000000..d72e48a9 --- /dev/null +++ b/test/DivertR.UnitTests/RedirectStaticTests.cs @@ -0,0 +1,90 @@ +using DivertR.UnitTests.Model; +using Shouldly; +using Xunit; + +namespace DivertR.UnitTests; + +public class RedirectStaticTests +{ + [Fact] + public void GivenProxy_WhenProxyMethodCalled_ThenRelayToRoot() + { + // ARRANGE + var proxy = Redirect.Proxy(new Foo("test")); + + // ACT + var callResult = proxy.Name; + + // ASSERT + callResult.ShouldBe("test"); + } + + [Fact] + public void GivenProxyWithNoRoot_WhenProxyCalled_ShouldReturnDefault() + { + // ARRANGE + var proxy = Redirect.Proxy(); + + // ACT + var callResult = proxy.Name; + + // ASSERT + callResult.ShouldBeNull(); + } + + [Fact] + public void GivenProxyWithVia_WhenMockCalled_ThenRedirects() + { + // ARRANGE + var proxy = Redirect.Proxy(); + var redirect = Redirect.Of(proxy); + + redirect + .To(x => x.Echo(Is.Any)) + .Via<(string input, __)>(call => $"{call.Args.input} redirected"); + + // ACT + var result = proxy.Echo("test"); + + // ASSERT + result.ShouldBe("test redirected"); + } + + [Fact] + public void GivenProxyAsObject_WhenRedirectOf_ThenThrowsDiverterException() + { + // ARRANGE + object fooProxy = Redirect.Proxy(); + + // ACT + var testAction = () => Redirect.Of(fooProxy); + + // ASSERT + testAction.ShouldThrow(); + } + + [Fact] + public void GivenNonProxyObject_WhenRedirectOfObject_ThenThrowsDiverterException() + { + // ARRANGE + IFoo foo = new Foo(); + + // ACT + var testAction = () => Redirect.Of(foo); + + // ASSERT + testAction.ShouldThrow(); + } + + [Fact] + public void GivenDispatchProxyFactory_WhenSpyOnClassType_ThenThrowsDiverterException() + { + // ARRANGE + + // ACT + var testAction = () => Redirect.Proxy(); + + // ASSERT + testAction.ShouldThrow(); + } +} \ No newline at end of file