Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generic type argument matchers (It.IsAnyType and custom matchers) #908

Merged
merged 20 commits into from
Aug 31, 2019

Conversation

stakx
Copy link
Contributor

@stakx stakx commented Aug 25, 2019

These commits add support for generic type argument matching. 😎

For starters, the only built-in type matcher is It.IsAnyType:

mock.Setup(m => m.Method<It.IsAnyType>(...))...

Creating a custom type matcher:

The type matcher infrastructure is designed to be fully extensible:

class Exceptions : ITypeMatcher
{
    public bool Matches(Type typeArgument) => typeof(Exception).IsAssignableFrom(typeArgument);
}

handler.Setup(h => h.Handle<Exceptions>())
       .Callback(() => Console.WriteLine("Handled an exception.");

This allows matchers to satisfy generic type contraints:

public interface IJuggler
{
    void Juggle<TBall>() where TBall : Ball
}

class AnyBall : Ball, ITypeMatcher
{
    public bool Matches(Type typeArgument) => true;
}

new Mock<IJuggler>().Setup(j => j.Juggle<AnyBall>())...;

When the type is constrained to something that won't allow you to implement ITypeMatcher, there's [TypeMatcher]:

public interface IRedirector
{
    void Redirect<TDelegate>(TDelegate handler) where TDelegate : Delegate;
}

[TypeMatcher(typeof(It.IsAnyType))]
public delegate void AnyDelegate();

var redirector = new Mock<IRedirector>();
redirector.Setup(r => r.Redirect<AnyDelegate>(...)))...

Accessing the type arguments in Callback and Returns:

(@michal-ciechan, this one is for you: You requested that some "invocation context" be injected into Callback and Returns callbacks, it turns out Moq already has a suitable type for that: IInvocation.)

In order for Callback and Returns to be able to know which type arguments were used, there are two new overloads that accept a InvocationAction and InvocationFunc, respectively:

public interface IFactory
{
    T Create<T>();
}

var factory = new Mock<IFactory>();
factory.Setup(f => f.Create<It.IsAnyType>())
       .Returns(new InvocationFunc(invocation =>
                {
                    var type = invocation.Method.GetGenericTypeArguments()[0];
                    return Activator.CreateInstance(type);
                });

var something = factory.Object.Create<Something>();

These two types are designed as delegate type look-alikes, however they aren't actually delegates. This was necessary to prevent the C# compiler from starting to pick the wrong Callback and Returns overloads in everyday use cases.

Matching arguments of a generic type:

interface IFrobbler
{
    void Frobble<T>(T arg);
}

var frobbler = new Mock<IFrobbler>();
frobbler.Setup(f => f.Frobble(It.IsAny<It.IsAnyType>()))
        .Callback((object arg) => ...);

Things left to do:

  • Add support for It.IsAny<It.IsAnyType>. Or, more generally speaking: It.IsAny<TTypeMatcher>, It.IsNotNull<TTypeMatcher>, etc.

  • If some or all of the above is implemented, add tests for the Capture code paths. (More specifically, I am worrying about the type conversions (T) hidden inside the Match class. Those will generally fail when T is a type matcher.)

    Update: This is only partially fulfilled. While Capture.* shouldn't throw, CaptureMatch<T> won't work just yet. This is left as a TODO for another time.

  • Add support for composite types making use of type matchers, e.g. It.IsAnyType?, It.IsAnyType[], It.IsAnyType[,], IEnumerable<It.IsAnyType>, ref/in/out It.IsAnyType, etc. Same for custom matcher types. It's possible that this cannot be made to work with custom argument matchers.

    Update: While that would be great, it would also negatively affect performance as every type parameter would have to be decomposed just to discover whether it includes a type matcher. For now, we're going the opposite direction and try to make type matcher discovery as fast as possible.

Closes #343.

/cc @michal-ciechan, @oddbear, @kzu

@stakx stakx added this to the 4.13.0 milestone Aug 25, 2019
CHANGELOG.md Outdated Show resolved Hide resolved
tests/Moq.Tests/ItIsAnyTypeFixture.cs Show resolved Hide resolved
{
foreach (var typeArgument in methodCallExpression.Method.GetGenericArguments())
{
if (typeArgument.IsTypeMatcher())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this loop is going to run for every generic method Moq encounters, it might be a good idea to cache information about whether or not some Type is a type matcher.

src/Moq/Guard.cs Outdated
@@ -16,6 +16,18 @@ namespace Moq
[DebuggerStepThrough]
internal static class Guard
{
public static void HasDefaultConstructor(Type type)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be useful in MockDefaultValueProvider, too. Today, DefaultValue.Mock will simply attempt to mock types even if they don't have a default constructor, resulting in a DynamicProxy exception (which could be hidden).

An alternative would be to instead add a type.HasDefaultConstructor() check inside type.IsMockable(). That way, MockDefaultValueProvider would no longer attempt to mock default ctor-less types and delegate to DefaultValue.Empty.

tests/Moq.Tests/ItIsAnyTypeFixture.cs Outdated Show resolved Hide resolved
tests/Moq.Tests/ItIsAnyTypeFixture.cs Outdated Show resolved Hide resolved
@michal-ciechan
Copy link

Nice! Thanks for caring about this issue so long after! Will take a look at the code and take it out for a spin tomorrow!

@stakx stakx force-pushed the generic-type-argument-matchers branch 2 times, most recently from 6805fc8 to bb53067 Compare August 26, 2019 05:54
@stakx
Copy link
Contributor Author

stakx commented Aug 26, 2019

It just occurred to me that we could special-case It.IsAny<It.IsAnyType>() to be roughly equivalent to @helloserve's It.Ignore() (see #790). Some thoughts regarding this idea:

  • Since It.IsAny gets used a lot, perhaps we should require that all type matchers carry a [TypeMatcher] attribute so that they can be identified more quickly.

  • Consider special-casing other built-on matchers, too, where that makes sense.

  • What to do about custom argument matchers? Those would have to deal with It.IsAnyType et al. themselves.

  • Ideally, argument matchers wouldn't support just It.IsAnyType, but any type matcher.

  • It.IsAnyType should be declared as a struct so that It.IsAny<It.IsAnyType?> could work as well if one wants to explictly include null values.
    (Edit: Not necessarily. It's likely we'll at some point need to add more standard type matchers such as It.IsAnyClass and It.IsAnyStruct to account for the most common generic type constraints. So whether It.IsAnyType is declared as a class or struct probably ought to depend on which generic type constraint is more common in general practice.)

@stakx stakx force-pushed the generic-type-argument-matchers branch from bb53067 to 8955f5f Compare August 31, 2019 11:00
@stakx stakx force-pushed the generic-type-argument-matchers branch from 8955f5f to 3610193 Compare August 31, 2019 12:10
@stakx stakx force-pushed the generic-type-argument-matchers branch from 3610193 to 10bc369 Compare August 31, 2019 12:16
@stakx stakx force-pushed the generic-type-argument-matchers branch from 10bc369 to 1d572e5 Compare August 31, 2019 12:53
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Open Generic Method Callback
2 participants