steve
steve

Reputation: 917

How to mock extension methods with Moq | Url.Page()

I am trying to mock a class which registers users to test. and In the test code I can see it clearly fails at callbackUrl below.

The PageModel class has a field IUrlHelper declared as Url. The IUrlHelper interface has 5 methods which does NOT include .Page(). Mocking those 5 would be easy but I have no idea how to mock extension methods.

Can someone please help? I have been stuck on this for ages.

RegisterModel

public class RegisterModel : PageModel
{
    private readonly IUrlHelper _urlHelper;

    public RegisterModel(
        IUrlHelper urlHelper)
        {}

    public async Task<IActionResult> OnPostAsync(
        string returnUrl = null)
    {
        returnUrl = returnUrl ?? Url.Content("~/");
        var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { userId = "full code has IdentityUserCreated", code = "string" },
                    protocol: Request.Scheme);
        LocalRedirect(returnUrl);                          
        return Page();
    }
}

RegisterModelTests

[TestFixture]
public class RegisterModelTests
{
    private Mock<IUrlHelper> _mockUrlHelper;

    [SetUp]
    public void SetUp()
    {
        _mockUrlHelper = new Mock<IUrlHelper>();
        SetUpUrlHelper();
    }

    public RegisterModel CreateRegisterModel()
    {
        return new RegisterModel(
            _mockUrlHelper.Object
        );
    }

    [Test]
    public async Task GivenValidInput_OnPostAsync_CreatesANewUser()
    {
        // Arrange
        var unitUnderTest = CreateRegisterModel();

        // Act
        var result = await unitUnderTest.OnPostAsync("/asdsad/asda");

        // Assert
        if (result != null)
            Assert.Pass();
    }

    private void SetUpUrlHelper()
    {
        _mockUrlHelper.Setup(x => x.Page(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IdentityUser>(),
               It.IsAny<string>())).Returns("callbackUrl").Verifiable();
    }

Upvotes: 1

Views: 9637

Answers (2)

Greg Gorman
Greg Gorman

Reputation: 306

I tried ICodeGorilla's solution but found that Static Types cannot be used as type arguments. So I modified the code a bit to this:

        public static void Replace(Type original, Type target)
        {
            var targetMethods = GetStaticPublicMethods(target);
            foreach (var targetMethod in targetMethods)
            {
                var parameters = targetMethod.GetParameters().Select(x => x.ParameterType).ToArray();
                var originalMethod = original.GetMethod(targetMethod.Name, parameters);
                if (originalMethod != null)
                {
                    SwapMethodBodies(originalMethod, targetMethod);
                }
                else
                {
                    Debug.WriteLine(
                        "*****************************************************************************************");
                    Debug.WriteLine($"Method not found - {targetMethod.Name}");
                    Debug.WriteLine(
                        "*****************************************************************************************");
                }
            }
        }

        private static List<MethodInfo> GetStaticPublicMethods(Type t)
        {
            return t.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .Distinct().ToList();
        }

The usage is now:

ShimHelper.Replace(
                typeof(ExtensionClass), 
                typeof(MockedExtensionClass));

I found this worked very nicely for AjaxRequestExtensions in MVC.

Upvotes: 1

ICodeGorilla
ICodeGorilla

Reputation: 598

The short answer is that you can not mock an extension method as they are static methods and moq can only handle object. But this post will tell you more.

You may want to change the way your testing, moving it to a controller level. But I think there is a technical solution. We can do it but maybe we shouldn't.

You should however be able to shim the method. It is swapping out a method address with another. Make sure you really need it and ensure that it is only used in testing.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

public static class ShimHelper
{
    public static void Replace<TOriginal, TTarget>()
    {
        var typeOfOriginal = typeof(TOriginal);
        Replace<TTarget>(typeOfOriginal);
    }

    public static void Replace<TTarget>(Type typeOfOriginal)
    {
        var targetMethods = GetStaticPublicMethods<TTarget>();
        foreach (var targetMethod in targetMethods)
        {
            var parameters = targetMethod.GetParameters().Select(x => x.ParameterType).ToArray();
            var originalMethod = typeOfOriginal.GetMethod(targetMethod.Name, parameters);
            if (originalMethod != null)
            {
                SwapMethodBodies(originalMethod, targetMethod);
            }
            else
            {
                Debug.WriteLine(
                    "*****************************************************************************************");
                Debug.WriteLine($"Method not found - {targetMethod.Name}");
                Debug.WriteLine(
                    "*****************************************************************************************");
            }
        }
    }

    private static List<MethodInfo> GetStaticPublicMethods<T>()
    {
        return typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Static)
            .Distinct().ToList();
    }

    private static void SwapMethodBodies(MethodInfo a, MethodInfo b)
    {
        RuntimeHelpers.PrepareMethod(a.MethodHandle);
        RuntimeHelpers.PrepareMethod(b.MethodHandle);

        unsafe
        {
            if (IntPtr.Size == 4)
            {
                Replace32Bit(a, b);
            }
            else
            {
                Replace64Bit(a, b);
            }
        }
    }

    private static unsafe void Replace64Bit(MethodInfo a, MethodInfo b)
    {
        var inj = (long*)b.MethodHandle.Value.ToPointer() + 1;
        var tar = (long*)a.MethodHandle.Value.ToPointer() + 1;
        *tar = *inj;
    }

    private static unsafe void Replace32Bit(MethodInfo a, MethodInfo b)
    {
        var inj = (int*)b.MethodHandle.Value.ToPointer() + 2;
        var tar = (int*)a.MethodHandle.Value.ToPointer() + 2;
        *tar = *inj;
    }
}

Usage:

ShimHelper.Replace<ExtensionClass, MockedExtensionClass>();

Where your mocked extension class matches the method signature exactly. Run this in your test fixture setup and you should be good.

Upvotes: 1

Related Questions