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

implicit guard too strict; prevents valid user wildcard _ implementations #1199

Closed
adamfk opened this issue Aug 18, 2021 · 12 comments · Fixed by #1202
Closed

implicit guard too strict; prevents valid user wildcard _ implementations #1199

adamfk opened this issue Aug 18, 2021 · 12 comments · Fixed by #1202
Assignees
Labels
Milestone

Comments

@adamfk
Copy link
Contributor

adamfk commented Aug 18, 2021

Edit: if you want to see where the bug is discussed, skip to #1199 (comment)

The fix for the issue seems counterintuitive at first, but was thoughtfully considered here: #1202 (comment)

The rest that follows is a bunch of discussion/attempts to enable an alternative user-code shorthand _ for It.IsAny<>().


Hello! Awesome project.

I have a suggestion for how to support an alternate shorthand _ instead of It.IsAny<T>().

This is inspired by gmock's _ wildcard matcher.

When mocking a function that takes more than a few arguments, the It.IsAny<T>() noise really distracts and is unpleasant to write.

mockCar.Setup(car => car.DoSomething(It.IsAny<byte>(), It.IsAny<uint>(), It.IsAny<int>(), It.IsAny<int>())).Returns(0x42);

versus

mockCar.Setup(car => car.DoSomething(_, _, _, _)).Returns(0x42);
// or
mockCar.Setup(car => car.DoSomething(any, any, any, any)).Returns(0x42);

The code below compiles fine and triggers the correct It.IsAny<T>() calls, but it doesn't result in a proper moq match. I traced it through the moq codebase for a bit and then figured I should ask here first.

using Moq;
using Xunit;
using FutureMoq;

// I'm hoping this part would end up in Moq
namespace FutureMoq
{
    // This class exists soley to expose `_` or `any` properties as shorthands.
    // See `using static AnyHelper<UserCustomAnyHelper>;` in code below.
    public class AnyHelper<T> where T : AnyValueBase, new()
    {
        public static T _ => new T();
        public static T any => new T();
    }

    // This class provides implicit conversions that trigger the correct `It.isAny<T>()` for common types
    public class AnyValueBase
    {
        public static implicit operator byte(AnyValueBase _) => It.IsAny<byte>();
        //...
        public static implicit operator uint(AnyValueBase _) => It.IsAny<uint>();
        public static implicit operator int(AnyValueBase _) => It.IsAny<int>();
    }
}

// Some example user test code
namespace TestNamespace
{
    using static AnyHelper<UserCustomAnyHelper>;   // note `using static` to simplify syntax

    // This class provides any additional implicit conversions for any types missing from
    // `AnyValueBase`. In this example, it contains an implicit conversion for a custom enum.
    public class UserCustomAnyHelper : AnyValueBase
    {
        public static implicit operator GearId(UserCustomAnyHelper _) => It.IsAny<GearId>();
    }

    public enum GearId
    {
        Reverse,
        Neutral,
        Gear1,
    }

    public interface ICar
    {
        byte SetGear(GearId a, int b);
    }

    public class TestClass
    {
        [Fact]
        public void NewWay()
        {
            var mockCar = new Mock<ICar>();
            var car = mockCar.Object;

            mockCar.Setup(car => car.SetGear(_, _)).Returns(42);
            Assert.Equal(42, car.SetGear(GearId.Gear1, 5)); //Gear1 and 5 are arbitrary
        }

        [Fact]
        public void OldWay()
        {
            var mockCar = new Mock<ICar>();
            var car = mockCar.Object;

            mockCar.Setup(car => car.SetGear(It.IsAny<GearId>(), It.IsAny<int>())).Returns(42);
            Assert.Equal(42, car.SetGear(GearId.Gear1, 5)); //Gear1 and 5 are arbitrary
        }
    }
}

Even if you aren't interested in adding the AnyHelper classes to Moq, would you be receptive to a PR that would allow this type of functionality to work? I believe a change is required in the Expression handling.

@stakx
Copy link
Contributor

stakx commented Aug 18, 2021

Hi @adamfk, nice idea! I agree that It.IsAny<T>() is rather verbose, being able to use a discard-like _ construct would be lovely indeed.

Unfortunately, faking a discard operator _ / any based on implicit convertability is not going to work for interfaces, because the C# language does not allow you to define an implicit operator ISomeInterface. This is a major roadblock, because in tests, one is likely to encounter interfaces rather frequently.

That aside, your example above shows that users would also have to do some configuration to get this to work for own types: one would have to derive a UserCustomAnyHelper class with the required conversions; I think this would really hinder discoverability and ease of use, and thus adoption. It.IsAny<T>() is such a central part of Moq that it should "just work", right out of the box. Same goes for look-alikes, too, IMHO.

For those two reasons (_ won't work for everything, and it possibly isn't easy enough to set up), I'm hesitant about adding it to the main library.

With regard to enabling _ in your own code: As far as I can tell, there are at least two things that need to change:

  1. You need to make the following change such that Moq will recognize _ / any as matchers:

    -public static T _ => new T();
    +public static T _ => It.IsAny<T>();
    -public static T any => new T();
    +public static T any => It.IsAny<T>();

    (You don't have to make use of It.IsAny<T>(); you could also define your custom matcher using Match.Create(x => true, () => _).)

  2. We added the following code in Fail when implicit conversion operator renders argument matcher unmatchable #900 that would eventually trigger for your example code above:

    https://github.com/moq/moq4/blob/2a70b29ce70db85c93b6fcacf7e4a7e8e588cee3/src/Moq/MatcherFactory.cs#L96-L107

    I'd be curious to see if this guard could be adjusted, so that it would still catch the scenarios it was designed for, but not trigger in yours. Perhaps you could find out, and if you succeed in identifying the necessary changes, we work towards a PR from there.

Just for the sake of completeness, this is what you can currently do. It's not super-succinct, but a little bit shorter than It.IsAny<T>():

public abstract class TestFixtureBase
{
    public static AnyBool => Match.Create(_ => true, () => AnyBool);
    public static AnyInt => Match.Create(_ => true, () => AnyInt);
    public static AnyGear => Match.Create(_ => true, () => AnyGear);
    ...
}

public class TestClass : TestFixtureBase
{
    ...

    [Fact]
    public void OldWay2()
    {
        ...
        mockCar.Setup(car => car.SetGear(AnyGear, AnyInt)).Returns(42);
        ...
    }
}

@adamfk
Copy link
Contributor Author

adamfk commented Aug 18, 2021

Excellent points! I hadn't thought of implicit conversions with interfaces.

It might be able to work with interfaces, but it starts to get even more janky. UserCustomAnyHelper would have to be made to implement the interface. Maybe this will be more attractive with a C# source generator some time in the future (they seem unstable right now).

For interests sake, my original code above does get past the guard mentioned above because convertExpression.Operand.IsMatch(out _) returns false. It gets relatively deep into the Expression tree code, fires the correct conversions that create the correct It.IsAny<T>(); matchers (I set breakpoints to confirm), but just doesn't seem to work.

The guard does throw however if I make the changes you suggested in the diff above. I'll poke around a bit more and see what I can figure out.

Thanks for the fast and helpful response! Feel free to close. I'll report back here if I make any substantial progress.

@stakx
Copy link
Contributor

stakx commented Aug 18, 2021

OK, I'll close this for the time being. We can always reopen if and when you find a viable solution. Good luck! :-)

@adamfk
Copy link
Contributor Author

adamfk commented Aug 18, 2021

@stakx it wasn't as hard as I thought to find reasonable work arounds. I've opened a PR more for discussion than expecting a merge.

@adamfk
Copy link
Contributor Author

adamfk commented Aug 19, 2021

Here's a cleaner solution: adamfk@c4dca50#diff-e19e65740ba7805f6c3c79daa3472dc0e01fe06285f3cdf5831fb8286501852c

I think this coupled with a 3rd party Roslyn analyzer/code fix lib could work really well. The only thing that it would require from moq is adding the AnyValue attribute.

Thoughts?

@stakx stakx reopened this Aug 19, 2021
@stakx
Copy link
Contributor

stakx commented Aug 19, 2021

I think Roslyn is great! Take a look at the upcoming Moq v5 and its foundation library Avatar regarding source generators. For Moq v4, we'd probably profit most from analyzers for detecting common usage errors.

My primary issue with your PR is the introduction of AnyValueAttribute. Moq already has a mechanism to detect matchers, and I don't think it makes sense to add another, alternate one. (MatcherAttribute has been marked obsolete in the past for the same reason, and there used to be an AdvancedMatcherAttribute that got removed, too.)

If possible, we should reuse the existing mechanism, which is to go through Match.Create<>() (either directly, or indirectly via It.IsAny<>(). That may not be the quickest route, but if it keeps the public API slim and deduplicated it's worth it IMHO.

@adamfk
Copy link
Contributor Author

adamfk commented Aug 19, 2021

I can definitely understand wanting to keep the public API slim.

Do you think that Moq v5 will support wildcard matchers like _? Or will it still require something like It.IsAny<T>()?

Because C# doesn't allow implicit conversion to interfaces (as you mentioned above), I don't see a road forward using the existing public API.

These are some options I see:

  1. No wildcard matcher support. Not a great user experience from my perspective.
  2. Fairly simple and documentable AnyValueAttribute.
  3. User/3rd party extension method that essentially replaces Mock.Setup() and the like.
  4. Fork of Moq.

Of those options, I only like the second :)

Coming from using gmock a lot, I see a lot of value in its wildcard matcher. I realize moq has different design goals. Do you see any/much value in a wildcard matcher?

Thanks for your time :)

@stakx
Copy link
Contributor

stakx commented Aug 19, 2021

Do you think that Moq v5 will support wildcard matchers like _? Or will it still require something like It.IsAny<T>()?

It's the latter (Arg.Any<T>()).

It doesn't really matter who is trying to write the source code, you or a Roslyn generator. The fact of the matter is that C#'s static type system simply doesn't make it easy to create a construct like your _ that can assume any type.

Coming from using gmock a lot, I see a lot of value in its wildcard matcher.

gMock is a C++ library, right? C++ is more flexible in that regard; I'm not sure you'll find something comparable in mainstream .NET languages. For that reason, I suspect you'll need to get used with the reality of your option (1).

(2) is an option but like I stated before, if we find a way where it isn't needed, that would be preferable. See my earlier post for the likely direction we'd need to investigate.

(3) would be an option, however strong static typing support is one of Moq's explicitly stated design goals, so it's unlikely we'll be adding something like mock.Setup("SomeMethod").

@adamfk
Copy link
Contributor Author

adamfk commented Aug 20, 2021

... strong static typing support is one of Moq's explicitly stated design goals, so it's unlikely we'll be adding something like mock.Setup("SomeMethod").

Totally agree. I want a solution that supports refactoring and other IDE/compiler goodness. 🤖

Without the implicit guard (if (convertExpression.Method?.Name == "op_Implicit")), the following works:

public abstract class AutoIsAny
{
	public static AnyValue _
	{
		get
		{
			It.IsAny<object>();
			return default;
		}
	}
}

adamfk@fe0ec2a

This obviously isn't a solution. Just showing one of the many things that I've tried.

Even if I find a way around the implicit guard without removing it, I don't like that solution because it is likely that another fix to moq will break whatever workaround I find. It's like relying on undefined behavior.

Without adding some way for moq to realize that an implicit conversion is OK, or that something should be treated as AnyValue, I don't think there's a reasonable solution.

I'm happy to investigate more if you can provide some direction. Sorry if I'm missing something totally obvious.

@stakx
Copy link
Contributor

stakx commented Aug 20, 2021

Even if I find a way around the implicit guard without removing it, I don't like that solution because it is likely that another fix to moq will break whatever workaround I find. It's like relying on undefined behavior.

But it wouldn't be undefined behavior: once you've demonstrated that the current implicit guard is too strict, and you've found a way to change it such that it allows your scenario while still working for the original use case, then we add a test with your use case to the test suite. Any future changes to Moq will from then on have to take your legitimate use case into consideration.

I'm happy to investigate more if you can provide some direction.

Let me start by making a correction to my first post above. I really should have suggested the following:

-public static T _ => It.IsAny<T>();
+public static T _ => (T)It.IsAny<object>();
-public static T any => It.IsAny<T>();
+public static T any => (T)It.IsAny<object>();

Because otherwise, the implicit guard is correct and nothing would ever match _: T is e.g. UserCustomAnyHelper in your tests, and when actual calls happen, no argument value will ever be of that type. On the other hand, any argument value is assignment-compatible to object, so that'll work.

(Btw. that's the only place where matchers are needed. Your implicit operators in AnyValueBase and UserCustomAnyHelper may simply return default instead.)

If we then temporarily comment out the implicit guard, your NewWay test will pass. This shows that it is indeed the single code location that stops your scenario from working, and if we find a way to make the guard more precise, we're done.

If you're still worried about this approach relying on undefined behavior, let's look at it in a different way. We're actually trying to fix a bug here, because the test we just ran has proven that the current implicit guard is faulty: it claims something to be unmatchable that wouldn't be, and it thus blocks a legitimate use case.

Let's briefly review what led to the guard being added.

interface IX
{
    void Method(DateTimeOffset arg);
}

var mock = new Mock<IX>();
mock.Setup(x => x.Method(It.IsAny<DateTime>())).Verifiable();
mock.Object.Method(DateTime.Now);
mock.Verfify();

The programmer is making a mistake here, because It.IsAny<DateTime>() will only ever run against DateTimeOffset values, which will never match. And the guard is supposed to let them know about their mistake.

I suspect the issue with the implicit guard is that it compares the wrong thing against the parameter type:

https://github.com/moq/moq4/blob/a7632bf668ad3e7ed84880398066c4dc45a36622/src/Moq/MatcherFactory.cs#L96-L102

(For the above example, parameter would be the arg parameter, and convertExpression.Operand would be the It.IsAny<DateTime>() expression.)

The thing is, this code only looks at the static type of the matcher expression, and not at the matcher's type argument. In the above example, those two types are identical; but in your case, they are not:

public static T _ => (T)It.IsAny<object>();

The implicit guard checks the assignment-compatibility of GearId against T (that is, UserCustomAnyHelper) instead of against object. In order to get at the latter type (object), it would actually have to inspect the identified matcher, but currently it's simply throwing it away (using a out _ discard).

The Match class might have to be augmented with an abstract Type Type { get; } property, implemented in Match<T> as Type => typeof(T). That Type property could then be used in the implicit guard in place of convertExpression.Operand.Type.

So I think that might be one way how to solve this puzzle. 😉

@adamfk
Copy link
Contributor Author

adamfk commented Aug 20, 2021

Thanks for the help! I'll take another crack at it this weekend :)

If I can get this working normally, maybe a source generator could do all the grunt work of making the compiler happy. That would make for an excellent user experience 🚀

@stakx
Copy link
Contributor

stakx commented Aug 20, 2021

Agreed. It might also make a nice contrib package!

adamfk added a commit to adamfk/moq4 that referenced this issue Aug 21, 2021
@stakx stakx added bug and removed enhancement labels Aug 21, 2021
@stakx stakx added this to the 4.17.0 milestone Aug 21, 2021
@stakx stakx removed the discussion label Aug 21, 2021
@adamfk adamfk changed the title possible shorthand implementation for It.IsAny<T>() implicit guard too strict; prevents valid user wildcard _ implementations Aug 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
2 participants