From 5f0c73f188c225e81aa97f8f4426d61d05f4684a Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Tue, 27 Feb 2024 17:00:12 -0800 Subject: [PATCH] Adding functionality to prevent caching --- Moq.AutoMock.Tests/ConstructorTests.cs | 112 ------------------ Moq.AutoMock.Tests/DescribeResolvedObjects.cs | 25 +++- Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj | 5 + .../Resolvers/MockResolutionContextTests.cs | 16 ++- Moq.AutoMock/AutoMocker.cs | 34 ++++-- Moq.AutoMock/Resolvers/ArrayResolver.cs | 3 +- .../Resolvers/MockResolutionContext.cs | 5 + 7 files changed, 59 insertions(+), 141 deletions(-) delete mode 100644 Moq.AutoMock.Tests/ConstructorTests.cs diff --git a/Moq.AutoMock.Tests/ConstructorTests.cs b/Moq.AutoMock.Tests/ConstructorTests.cs deleted file mode 100644 index 8305c423..00000000 --- a/Moq.AutoMock.Tests/ConstructorTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -/// -/// Based on https://gist.github.com/Keboo/d2f7f470d66fc4ff7d978ccb107b07cf -/// -namespace Moq.AutoMock.Tests; - -public interface IConstructorTest -{ - void Run(); -} - -[ExcludeFromCodeCoverage] -public static class ConstructorTest -{ - private class Test : IConstructorTest - { - public Dictionary SpecifiedValues { get; } - = new Dictionary(); - - private Type TargetType { get; } - - public Test(Type targetType) - { - TargetType = targetType; - } - - public Test(Test original) - { - TargetType = original.TargetType; - foreach (KeyValuePair kvp in original.SpecifiedValues) - { - SpecifiedValues[kvp.Key] = kvp.Value; - } - } - - public void Run() - { - foreach (ConstructorInfo constructor in TargetType.GetConstructors()) - { - ParameterInfo[] parameters = constructor.GetParameters(); - - object?[] parameterValues = parameters - .Select(p => p.ParameterType) - .Select(t => - { - if (SpecifiedValues.TryGetValue(t, out object? value)) - { - return value; - } - Mock mock = (Mock)Activator.CreateInstance(typeof(Mock<>).MakeGenericType(t))!; - return mock.Object; - }) - .ToArray(); - - for (int i = 0; i < parameters.Length; i++) - { - object?[] values = parameterValues.ToArray(); - values[i] = null; - - if (parameters[i].HasDefaultValue && parameters[i].DefaultValue is null) - { - //NB: no exception thrown - constructor.Invoke(values); - } - else - { - string parameterDisplay = $"'{parameters[i].Name}' ({parameters[i].ParameterType.Name})"; - TargetInvocationException ex = Assert.ThrowsException(new Action(() => - { - object? rv = constructor.Invoke(values); - throw new Exception($"Expected {nameof(ArgumentNullException)} for null parameter {parameterDisplay} but no exception was thrown"); - })); - if (ex.InnerException is ArgumentNullException argumentNullException) - { - Assert.AreEqual(parameters[i].Name, argumentNullException.ParamName); - } - else - { - throw new Exception($"Thrown argument for {parameterDisplay} was '{ex.InnerException?.GetType().Name}' not {nameof(ArgumentNullException)}.", ex.InnerException); - } - } - } - } - } - } - - public static IConstructorTest Use(this IConstructorTest test, T value) - { - if (test is Test internalTest) - { - var newTest = new Test(internalTest); - newTest.SpecifiedValues.Add(typeof(T), value); - return newTest; - } - else - { - throw new ArgumentException("Argument not expected type", nameof(test)); - } - } - - public static IConstructorTest BuildArgumentNullExceptionsTest() - => new Test(typeof(T)); - - public static void AssertArgumentNullExceptions() - => BuildArgumentNullExceptionsTest().Run(); -} diff --git a/Moq.AutoMock.Tests/DescribeResolvedObjects.cs b/Moq.AutoMock.Tests/DescribeResolvedObjects.cs index 9e4c8381..93be4a48 100644 --- a/Moq.AutoMock.Tests/DescribeResolvedObjects.cs +++ b/Moq.AutoMock.Tests/DescribeResolvedObjects.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics.CodeAnalysis; using Moq.AutoMock.Resolvers; -using Moq.AutoMock.Tests.Util; namespace Moq.AutoMock.Tests; @@ -78,7 +74,7 @@ public void ResolvedObjects_custom_resolver_providing_value_prevents_subsequent_ } [TestMethod] - public void ResolvedObjects_custom_resolver_can_prempt_cache_resolver() + public void ResolvedObjects_custom_resolver_can_preempt_cache_resolver() { object singleton = new(); object used = new(); @@ -90,6 +86,20 @@ public void ResolvedObjects_custom_resolver_can_prempt_cache_resolver() Assert.AreEqual(singleton, resolved); } + [TestMethod] + public void ResolvedObject_does_not_contain_custom_resolved_type_when_excluded() + { + object singleton = new(); + AutoMocker mocker = new(); + var cacheResolver = mocker.Resolvers.OfType().Single(); + var index = mocker.Resolvers.IndexOf(cacheResolver); + mocker.Resolvers.Insert(index + 1, new SingletonResolver(singleton) { NoCache = true }); + + object resolved = mocker.Get(); + Assert.AreEqual(singleton, resolved); + Assert.IsFalse(mocker.ResolvedObjects.Values.Contains(singleton)); + } + [ExcludeFromCodeCoverage] private class ThrowingResolver : IMockResolver { @@ -106,11 +116,14 @@ public SingletonResolver(T value) public T Value { get; } + public bool NoCache { get; set; } + public void Resolve(MockResolutionContext context) { if (context.RequestType == typeof(T)) { context.Value = Value; + context.NoCache = NoCache; } } } diff --git a/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj b/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj index 1d682a8a..73f68567 100644 --- a/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj +++ b/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj @@ -32,5 +32,10 @@ + + + Analyzer + false + \ No newline at end of file diff --git a/Moq.AutoMock.Tests/Resolvers/MockResolutionContextTests.cs b/Moq.AutoMock.Tests/Resolvers/MockResolutionContextTests.cs index 0526884b..8dfc9b9a 100644 --- a/Moq.AutoMock.Tests/Resolvers/MockResolutionContextTests.cs +++ b/Moq.AutoMock.Tests/Resolvers/MockResolutionContextTests.cs @@ -1,15 +1,13 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq.AutoMock.Resolvers; +using Moq.AutoMock.Resolvers; namespace Moq.AutoMock.Tests.Resolvers; [TestClass] -public class MockResolutionContextTests +[ConstructorTests(typeof(MockResolutionContext))] +public partial class MockResolutionContextTests { - [TestMethod] - public void Constructor_null_checks_arguments() - => ConstructorTest - .BuildArgumentNullExceptionsTest() - .Use(new ObjectGraphContext(false)) - .Run(); + partial void AutoMockerTestSetup(AutoMocker mocker, string testName) + { + mocker.Use(new ObjectGraphContext(false)); + } } diff --git a/Moq.AutoMock/AutoMocker.cs b/Moq.AutoMock/AutoMocker.cs index 0a16f9b0..15b2912c 100644 --- a/Moq.AutoMock/AutoMocker.cs +++ b/Moq.AutoMock/AutoMocker.cs @@ -128,12 +128,14 @@ private NonBlocking.ConcurrentDictionary? TypeMap private bool TryResolve(Type serviceType, ObjectGraphContext resolutionContext, - [NotNullWhen(true)] out IInstance? instance) + [NotNullWhen(true)] out IInstance? instance, + out bool noCache) { if (resolutionContext.VisitedTypes.Contains(serviceType)) { //Rejected due to circular dependency instance = null; + noCache = false; return false; } @@ -156,6 +158,7 @@ private bool TryResolve(Type serviceType, if (!context.ValueProvided) { instance = null; + noCache = false; return false; } @@ -165,6 +168,7 @@ private bool TryResolve(Type serviceType, IInstance i => i, _ => new RealInstance(context.Value), }; + noCache = context.NoCache; return true; } @@ -625,9 +629,9 @@ public object Get(Type serviceType, bool enablePrivate) private object Get(Type serviceType, ObjectGraphContext context) { - if (TryGet(serviceType, context, out IInstance? service)) + if (TryGet(serviceType, context, out IInstance? service, out bool noCache)) { - if (TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) + if (!noCache && TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) { typeMap[serviceType] = service; } @@ -639,11 +643,12 @@ private object Get(Type serviceType, ObjectGraphContext context) internal bool TryGet( Type serviceType, ObjectGraphContext context, - [NotNullWhen(true)] out IInstance? service) + [NotNullWhen(true)] out IInstance? service, + out bool noCache) { if (serviceType is null) throw new ArgumentNullException(nameof(serviceType)); - if (TryResolve(serviceType, context, out IInstance? instance)) + if (TryResolve(serviceType, context, out IInstance? instance, out noCache)) { service = instance; return true; @@ -655,9 +660,9 @@ internal bool TryGet( /// object? IServiceProvider.GetService(Type serviceType) { - if (TryGet(serviceType, new ObjectGraphContext(false), out IInstance? service)) + if (TryGet(serviceType, new ObjectGraphContext(false), out IInstance? service, out bool noCache)) { - if (TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) + if (!noCache && TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) { typeMap[serviceType] = service; } @@ -716,10 +721,10 @@ public Mock GetMock(Type serviceType, bool enablePrivate) private Mock GetMockImplementation(Type serviceType, bool enablePrivate) { - if (TryResolve(serviceType, new ObjectGraphContext(enablePrivate), out IInstance? instance) && + if (TryResolve(serviceType, new ObjectGraphContext(enablePrivate), out IInstance? instance, out bool noCache) && instance.IsMock) { - if (TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) + if (!noCache && TypeMap is { } typeMap && !typeMap.ContainsKey(serviceType)) { typeMap[serviceType] = instance; } @@ -1069,13 +1074,16 @@ bool TryCreateArguments(ConstructorInfo constructor, ObjectGraphContext context, for (int i = 0; i < parameters.Length; i++) { ObjectGraphContext parameterContext = new(context); - if (!TryGet(parameters[i].ParameterType, parameterContext, out IInstance? service)) + if (!TryGet(parameters[i].ParameterType, parameterContext, out IInstance? service, out bool noCache)) { context.AddDiagnosticMessage($"Rejecting constructor {GetConstructorDisplayString(constructor)}, because {nameof(AutoMocker)} was unable to create parameter '{parameters[i].ParameterType.FullName} {parameters[i].Name}'"); return false; } - EnsureCached(parameters[i].ParameterType, service); + if (!noCache) + { + EnsureCached(parameters[i].ParameterType, service); + } arguments[i] = service; } return true; @@ -1116,8 +1124,8 @@ private void EnsureCached(Type type, IInstance instance) private Mock GetOrMakeMockFor(Type type) { - if (TryResolve(type, new ObjectGraphContext(false), out IInstance? instance) && - instance is MockInstance mockInstance) + if (TryResolve(type, new ObjectGraphContext(false), out IInstance? instance, out bool noCache) && + instance is MockInstance mockInstance && !noCache) { EnsureCached(type, mockInstance); return mockInstance.Mock; diff --git a/Moq.AutoMock/Resolvers/ArrayResolver.cs b/Moq.AutoMock/Resolvers/ArrayResolver.cs index e0890ed5..db8bdf42 100644 --- a/Moq.AutoMock/Resolvers/ArrayResolver.cs +++ b/Moq.AutoMock/Resolvers/ArrayResolver.cs @@ -12,9 +12,10 @@ public void Resolve(MockResolutionContext context) { Type elmType = context.RequestType.GetElementType() ?? throw new InvalidOperationException($"Could not determine element type for '{context.RequestType}'"); MockArrayInstance arrayInstance = new(elmType); - if (context.AutoMocker.TryGet(elmType, context.ObjectGraphContext, out IInstance? instance)) + if (context.AutoMocker.TryGet(elmType, context.ObjectGraphContext, out IInstance? instance, out bool noCache)) { arrayInstance.Add(instance); + context.NoCache = noCache; } context.Value = arrayInstance; } diff --git a/Moq.AutoMock/Resolvers/MockResolutionContext.cs b/Moq.AutoMock/Resolvers/MockResolutionContext.cs index 3f002813..08a0247a 100644 --- a/Moq.AutoMock/Resolvers/MockResolutionContext.cs +++ b/Moq.AutoMock/Resolvers/MockResolutionContext.cs @@ -57,6 +57,11 @@ public object? Value /// internal bool ValueProvided { get; private set; } + /// + /// When set to true, the resolution will not use the cache to resolve resolved instance set in + /// + public bool NoCache { get; set; } + /// /// Deconstruct this instance into its individual properties. ///