Skip to content

Commit

Permalink
Prevent sealed class mocks (#237)
Browse files Browse the repository at this point in the history
* Add InstanceResolver

* revert comment

---------

Co-authored-by: Casey White <[email protected]>
  • Loading branch information
Casey White and Casey White committed Sep 27, 2023
1 parent f2313d7 commit 5fe9df5
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

/.vs
*.idea/

bin/
obj/
Expand Down
26 changes: 23 additions & 3 deletions Moq.AutoMock.Tests/DescribeCreateInstance.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Moq.AutoMock.Resolvers;
using Moq.AutoMock.Tests.Util;

namespace Moq.AutoMock.Tests;

Expand Down Expand Up @@ -218,6 +216,16 @@ public void It_includes_reason_why_nested_constructor_was_rejected()
Assert.AreEqual("Rejecting constructor Moq.AutoMock.Tests.DescribeCreateInstance+HasMultipleConstructors(Moq.AutoMock.Tests.DescribeCreateInstance+HasStringParameter hasString), because AutoMocker was unable to create parameter 'Moq.AutoMock.Tests.DescribeCreateInstance+HasStringParameter hasString'", ex.DiagnosticMessages[1]);
}

[TestMethod]
public void It_can_create_instances_of_nested_sealed_classes()
{
AutoMocker mocker = new();
mocker.Resolvers.Add(new InstanceResolver());
var mockWithSealedService = mocker.CreateInstance<HasNestedSealedService>();

Assert.AreEqual(mockWithSealedService.SealedService, mockWithSealedService.NestedSealedService.SealedService);
}

private class CustomStringResolver : IMockResolver
{
public CustomStringResolver(string stringValue)
Expand Down Expand Up @@ -267,6 +275,18 @@ public HasMultipleConstructorsNested(HasStringParameter hasString)
}
}

public class HasNestedSealedService
{
public SealedService SealedService { get; set; }
public WithSealedService NestedSealedService { get; set; }

public HasNestedSealedService(SealedService sealedService, WithSealedService nestedSealedService)
{
SealedService = sealedService;
NestedSealedService = nestedSealedService;
}
}

public record class ConcreteDependency(IService1 Service);
public record class ConcreteDependencyIsFirst(ConcreteDependency Dependency, IService1 Service);
public record class ConcreteDependencyIsSecond(IService1 Service, ConcreteDependency Dependency);
Expand Down
8 changes: 8 additions & 0 deletions Moq.AutoMock.Tests/DescribeGetMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,12 @@ public void It_returns_null_for_unmockable_object_via_iserviceprovider()
var service = mocker.GetService(typeof(string));
Assert.IsNull(service);
}

[TestMethod]
public void It_throws_when_mocking_a_sealed_class()
{
var mocker = new AutoMocker();
var act = () => mocker.GetMock<SealedService>();
Assert.ThrowsException<ArgumentException>(act);
}
}
9 changes: 9 additions & 0 deletions Moq.AutoMock.Tests/Util/SealedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Diagnostics.CodeAnalysis;

namespace Moq.AutoMock.Tests.Util;

[ExcludeFromCodeCoverage]
public sealed class SealedService
{
public SealedService() { }
}
14 changes: 14 additions & 0 deletions Moq.AutoMock.Tests/Util/WithSealedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Diagnostics.CodeAnalysis;

namespace Moq.AutoMock.Tests.Util;

[ExcludeFromCodeCoverage]
public class WithSealedService
{
public SealedService SealedService { get; set; }

public WithSealedService(SealedService service)
{
SealedService = service;
}
}
13 changes: 12 additions & 1 deletion Moq.AutoMock/AutoMocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using Moq.AutoMock.Extensions;
using Moq.AutoMock.Resolvers;
using Moq.Language;
using Moq.Language.Flow;
Expand Down Expand Up @@ -78,7 +79,7 @@ public AutoMocker(MockBehavior mockBehavior, DefaultValue defaultValue, DefaultV
new LazyResolver(),
new FuncResolver(),
new CancellationTokenResolver(),
new MockResolver(mockBehavior, defaultValue, defaultValueProvider, callBase),
new MockResolver(mockBehavior, defaultValue, defaultValueProvider, callBase)
};
}

Expand Down Expand Up @@ -219,6 +220,12 @@ public object CreateInstance(Type type, bool enablePrivate)
if (type is null) throw new ArgumentNullException(nameof(type));

var context = new ObjectGraphContext(enablePrivate);

return CreateInstanceInternal(type, context);
}

internal object CreateInstanceInternal(Type type, ObjectGraphContext context)
{
if (!TryGetConstructorInvocation(type, context, out ConstructorInfo? ctor, out IInstance[]? arguments))
{
throw new ObjectCreationException(
Expand Down Expand Up @@ -987,6 +994,10 @@ public void Verify<T, TResult>(Expression<Func<T, TResult>> expression, Times ti

internal Mock? CreateMock(Type serviceType, MockBehavior mockBehavior, DefaultValue defaultValue, DefaultValueProvider? defaultValueProvider, bool callBase, ObjectGraphContext objectGraphContext)
{
if (!serviceType.IsMockable())
{
return null;
}
var mockType = typeof(Mock<>).MakeGenericType(serviceType);

bool mayHaveDependencies = serviceType.IsClass
Expand Down
19 changes: 19 additions & 0 deletions Moq.AutoMock/Resolvers/InstanceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Moq.AutoMock.Resolvers;

/// <summary>
/// A resolver that resolves requested types with a created instance.
/// </summary>
public class InstanceResolver : IMockResolver
{
/// <summary>
/// Resolves requested types with created instances.
/// </summary>
/// <param name="context">The resolution context.</param>
public void Resolve(MockResolutionContext context)
{
if (context.AutoMocker.CreateInstanceInternal(context.RequestType, context.ObjectGraphContext) is { } instance)
{
context.Value = instance;
}
}
}

0 comments on commit 5fe9df5

Please sign in to comment.