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

Need a way to setup a property / method by name #580

Closed
BradleyUffner opened this issue Jan 20, 2018 · 7 comments
Closed

Need a way to setup a property / method by name #580

BradleyUffner opened this issue Jan 20, 2018 · 7 comments

Comments

@BradleyUffner
Copy link

BradleyUffner commented Jan 20, 2018

I'm building a unit test framework to assist with testing code that relies on Entity Framework, and I need a way to setup a callback on a method by name.

The primary function of my project is to dynamically create a mock object that behaves like user's subclass of DbContext. I have a ContextMockBuilder class, where T is the interface representing the DbContext under test. I create a Mock of T, then loop through all the properties on T, using reflection, to find all the properties of type DbSet , and populate them with a customized subclass TestDbSet. This works great, but I need to be able to call custom code for the SaveChanges method. Unfortunately, DbContext doesn't implement an interface with the SaveChanges method on it, which means I can't constraint T. That leads to not being able to use Moq's normal methods to setup the callback, since "mock.Setup(foo => ...)" foo will only contains members of System.Object.

I either need some method to setup a method by name, or some other way to do it that doesn't involve a strongly typed Expression. I thought I would be able to "abuse" Protected, since it allows member access by name, but apparently it throws an error if the member is public (otherwise, I believe it would work).

@stakx
Copy link
Contributor

stakx commented Jan 20, 2018

Moq already allows you to do what you want: Most Setup and Verify methods take an Expression<> as their first parameter. You can build such LINQ expression trees at runtime using the static factory methods of the System.Linq.Expressions.Expression class (e. g. Expression.MakeMemberAccess). Since you're already doing reflection, this should be a fairly small step.

(There's one exception, and that's if you want to set up a property set expectation. For example, SetupSet takes an Action<>, instead of an expression tree. You'd therefore have to first build an expression tree, then .Compile() it before passing it to SetupSet.)

Adding new overloads for Mock.Setup that take a member name as a string (similar to the Mock.Protected() functionality, but for public members) is not going to happen, as this would directly violate Moq's design goals.

@BradleyUffner
Copy link
Author

BradleyUffner commented Jan 20, 2018

Interesting, I hadn't thought of trying to get access that way. I'll see if I can get that working. Thank you for taking the time help.

@stakx
Copy link
Contributor

stakx commented Jan 20, 2018

Good luck. Report back if you get stuck, and I'll try to demo with a brief example.

@BradleyUffner
Copy link
Author

BradleyUffner commented Jan 20, 2018

Success (at least it seems to be working)!

var mock = new Mock<TDbContextInterface>();

var param = Expression.Parameter(typeof(TDbContextInterface), "i");
var method = typeof(TDbContextInterface).GetMethod("SaveChanges");
var call = Expression.Call(param, method);
var exp = Expression.Lambda<Action<TDbContextInterface>>(call, param);
mock.Setup(exp)
    .Callback(() =>
    {
        Debug.WriteLine("saving changes");
    });

Thanks again!

@stakx
Copy link
Contributor

stakx commented Jan 21, 2018

@BradleyUffner: That's looking good!

(Note that this is how Setup always gets called behind the scenes, you just don't notice because the compiler does all the hard work of creating code to build an Expression for you.)

@stakx stakx closed this as completed Jan 21, 2018
@BradleyUffner
Copy link
Author

BradleyUffner commented Jan 21, 2018

Just in case anyone else needs this functionality, I made an extension method to make things significantly easier that seems to handle all the cases I threw at it.

public static class MockExtensions
{
    public static ISetup<T> SetupByName<T>(this Mock<T> mock, string methodName, params Expression<Func<object>>[] args) where T : class
    {
        var callArgs = args.Select(a => a.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)a.Body).Operand : a.Body).ToArray();
        var argTypes = callArgs.Select(a => a.Type).ToArray();
        var param = Expression.Parameter(typeof(T), "i");
        var method = typeof(T).GetMethod(methodName, argTypes);
        if (method == null) { throw new ArgumentException($"{typeof(T).Name} does not contain a method \"{methodName}({string.Join(",", argTypes.Select(t => t.Name))})\".", nameof(methodName)); }
        var call = Expression.Call(param, method, callArgs);
        var exp = Expression.Lambda<Action<T>>(call, param);
        return mock.Setup(exp);
    }
}

It can be used like this (Equivalent of mock.Setup(i => i.ATestMethod(5, "Test"))) :

mock.SetupByName("ATestMethod", () => 5, () => "Test")
    .Callback(() =>
    {
        Debug.WriteLine("ATestMethod was called");
    });

or this (Equivalent of mock.Setup(i => i.ATestMethod(It.IsAny<int>, It.IsAny<string>)))

mock.SetupByName("ATestMethod", () => It.IsAny<int>(), () => It.IsAny<string>())
    .Callback(() =>
    {
        Debug.WriteLine("ATestMethod was called");
    });

I know the format is a little ugly, but it's the best way I could figure out how to make it support both the constant form, and the "It.IsAny" form. There are probably all kinds of improvements that could be made to the code of the extension method. I'm not going to make any claims against its efficiency, or that it won't blow up on some calls I haven't tested, but it works well enough for my uses.

@stakx
Copy link
Contributor

stakx commented Jan 21, 2018

@BradleyUffner: First of all, thanks for sharing your work! 👍

I know the format is a little ugly, but it's the best way I could figure out how to make it support both the constant form, and the "It.IsAny" form.

Let me point you at Moq's in-built .Protected() functionality, which faces the same problem. There, it got solved via ItExpr.IsAny<T>(). Basically, the Setup method accepts an object[] array for arguments. Every value in that array is "lifted" to a ConstantExpression except those that are MethodCallExpressions referring to the It.IsAny<T> static method; those are used verbatim. ItExpr.IsAny<T>() is a static factory method producing such an expression. This allows you to write:

mock.Protected().Setup("ProtectedMethod", 1, false, ItExpr.IsAny<string>(), 4.0);

(I might be wrong with some details here as I'm citing the algorithm from memory, but that's the basic idea. Take a look at Moq's source code to see how exactly arguments are transformed from objects to Expressions.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants