Matching Generic Type Arguments with Moq

June 5th 2020 Moq Unit Testing Xamarin

The Moq mocking library in version 4.13.0 added support for matching generic type arguments when mocking generic methods. The documentation doesn't go into much detail but thanks to additional information in IntelliSense tooltips and the originating GitHub issue I managed to quickly resolve the no implicit reference conversion error which I encountered at first.

I wanted to use this new functionality to test navigation in my FreshMvvm page model. When a command is called, it should navigate to a different page (corresponding to the given page model):

public class SamplePageModel : FreshBasePageModel
{
  public ICommand PushSecondPageCommand { get; }

  public SamplePageModel()
  {
    PushSecondPageCommand = new Command(
      () => CoreMethods.PushPageModel<SecondPageModel>()
    );
  }
}

In the test, I needed to mock the PushPageModel method and verify that it was called with the correct page model class as its type argument:

[Test]
public void PushSecondPageCommandShouldPushSecondPage()
{
  var coreMethodsMock = new Mock<IPageModelCoreMethods>();

  // add Setup call for the PushPageModel method

  var pageModel = new SamplePageModel();
  pageModel.CoreMethods = coreMethodsMock.Object;

  pageModel.PushSecondPageCommand.Execute(null);

  coreMethodsMock.VerifyAll();
}

The It.isSubType<T> type matcher seemed a good fit for my needs. It should match any subtype of the type argument T including that type itself. I came up with the following Setup call (the It.Is<bool> matcher is used to match the optional method parameter):

coreMethodsMock
  .Setup(m => m.PushPageModel<It.IsSubtype<SecondPageModel>>(
    It.Is<bool>(b => b)))
  .Returns(Task.CompletedTask)
  .Verifiable();

Unfortunately, this resulted in a compiler error:

The type 'Moq.It.IsSubtype' cannot be used as type parameter 'T' in the generic type or method 'IPageModelCoreMethods.PushPageModel(object, bool, bool)'. There is no implicit reference conversion from 'Moq.It.IsSubtype' to 'FreshMvvm.FreshBasePageModel'.

Thinking about it, the error made perfect sense. The generic type argument is constrained to subtypes of FreshBasePageModel and the It.IsSubtype<T> type matcher doesn't derive from it. Reference documentation for the It.IsAnyType type matcher gives instructions on how to handle such cases:

If the generic type parameter has more specific constraints, you can define your own type matcher inheriting from the type to which the type parameter is constrained.

Hence, I created a customized version of the It.isSubType<T> type matcher which I wanted to use originally but couldn't:

[TypeMatcher]
public class IsFreshBasePageModel<T> : FreshBasePageModel, ITypeMatcher
  where T: FreshBasePageModel
{
  bool ITypeMatcher.Matches(Type typeArgument)
  {
    return typeof(T).IsAssignableFrom(typeArgument);
  }
}

The key difference between the two is that my type matcher derives from the FreshBasePageModel to satisfy the PushPageModel method's constraint. It applies the same constraint to its generic type argument so that the test fails at compile-time instead of at runtime if an invalid type is used (because of PushPageModel method's constraint the application code would fail to compile if the generic type argument didn't derive from FreshBasePageModel).

I could now use my new type matcher in the Setup method call:

coreMethodsMock
  .Setup(m => m.PushPageModel<IsFreshBasePageModel<SecondPageModel>>(
    It.Is<bool>(b => b)))
  .Returns(Task.CompletedTask)
  .Verifiable();

The compiler didn't complain anymore and the test verified the generic type argument as I wanted it to. Here's the complete final test code for reference:

[Test]
public void PushSecondPageCommandShouldPushSecondPage()
{
  var coreMethodsMock = new Mock<IPageModelCoreMethods>();

  coreMethodsMock
    .Setup(m => m.PushPageModel<IsFreshBasePageModel<SecondPageModel>>(
      It.Is<bool>(b => b)))
    .Returns(Task.CompletedTask)
    .Verifiable();

  var pageModel = new SamplePageModel();
  pageModel.CoreMethods = coreMethodsMock.Object;

  pageModel.PushSecondPageCommand.Execute(null);

  coreMethodsMock.VerifyAll();
}

A minimal sample project featuring this test is available on GitHub.

Moq's built-in type matchers (It.IsAnyType, It.IsValueType and It.IsSubtype<T>) can only be used when the mocked method's generic type arguments don't have any constraints. When the mocked methods have constraints, these type matchers will cause no implicit reference conversion errors because they don't satisfy the constraints. In such cases, custom type matchers that satisfy the constraints need to be implemented and used instead.

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License