Mocking Microsoft.Toolkit.Mvvm.IMessenger
Asked Answered
L

3

5

It seems that for some reason, Microsoft has created an interface for it's messenger and then gone and implemented the logic as extension methods on the interface itself.

Unfortunately, I cannot use this beautiful solution: http://agooddayforscience.blogspot.com/2017/08/mocking-extension-methods.html - because IMessenger extensions calls implemented code on Messenger with an internal type as argument.

Why would Microsoft go to such lengths to make unit testing hard? (If you know a good, technical reason for this, please comment with the answer. I am very curious).

I want to unit test the ViewModels, which injects IMessenger. So how do I do this?

My solution is: Wrap IMessenger in a wrapper with an interface and inject that instead.

Is there a simpler/better solution? (I want it to be easy to understand and maintain).

Longshore answered 7/9, 2021 at 8:5 Comment(1)
Mock the core members of the [interface]( learn.microsoft.com/en-us/dotnet/api/…) that are eventually called by the extensions. As to why they designed it that way, only the MS devs that designed it can answer thatScornik
B
6

Moq is incredibly extensible, and provides an extension point for exactly this purpose. You can provide custom type matching logic by creating a type that implements ITypeMatcher. To match the IMessenger.Send signature, for instance:

[TypeMatcher]
public sealed class IsAnyToken : ITypeMatcher, IEquatable<IsAnyToken>
{
    public bool Matches(Type typeArgument) => true;
    public bool Equals(IsAnyToken? other) => throw new NotImplementedException();
}

You can use this to write Setup and Verify code exactly like It.IsAnyType:

mockMessenger.Setup(x => x.Send(It.IsAny<MyMessage>(), It.IsAny<IsAnyToken>());
...
mockMessenger.Verify(x => x.Send(It.IsAny<MyMessage>(), It.IsAny<IsAnyToken>(), Times.Once);
Boleyn answered 1/2, 2022 at 23:41 Comment(0)
O
0

Wrapping the interface (as others have suggested) is certainly an answer, but I don't like the cognitive overhead of having two interfaces so I came up with this. It does use a bit of reflection, but I couldn't find any other way to get at that object.

Long story short, it manually builds the setup expression against the internal type:

// Compile error, because Unit is internal
mock.Setup(x => x.Send<MyMessage, Unit>(It.IsAny<MyMessage>(), default));

But reflection and expression trees can still give it to us:

private static Type UnitType { get; } = typeof(IMessenger).Assembly
    .GetType("Microsoft.Toolkit.Mvvm.Messaging.Internals.Unit");

public static ISetup<IMessenger, TMessage> SetupMessage<TMessage>(this Mock<IMessenger> messenger,
    Expression<Func<TMessage, bool>>? validation = null)
    where TMessage : class
{
    MethodInfo sendMethod = typeof(IMessenger).GetMethod(nameof(IMessenger.Send))
        .MakeGenericMethod(typeof(TMessage), UnitType);

    ParameterExpression parameter = Expression.Parameter(typeof(IMessenger));

    Expression<Func<TMessage>> message = validation switch
    {
        null => () => It.IsAny<TMessage>(),
        not null => () => It.Is(validation),
    };

    MethodCallExpression methodCall = Expression.Call(
        parameter,
        sendMethod,
        message.Body,
        Expression.Default(UnitType));

    Type funcType = Expression.GetFuncType(typeof(IMessenger), typeof(TMessage));
    Expression lambda = Expression.Lambda(funcType, methodCall, parameter);

    return messenger.Setup((Expression<Func<IMessenger, TMessage>>) lambda);
}
Ourself answered 25/1, 2022 at 16:19 Comment(0)
A
0

I also didn't want to have to make a wrapper for IMessenger, and after the recent controversy with Moq (version 4.20) we're now using FakeItEasy, so the current answers didn't solve my problem.

I ended up making a test implementation of IMessenger

public class TestMessenger : IMessenger
{
    public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
        where TMessage : class
        where TToken : IEquatable<TToken>
    {
        if (message is MyMessage myMessage)
        {
            myMessage.Reply(new MyMessageReply);
        }

        return message;
    }

    // Other methods implemented as stubs
}

I'm sure you could do something slightly more advanced if you needed to, but this was enough for my purposes

Absquatulate answered 20/5 at 11:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.