如何用Moq模拟扩展方法Url.Page()

时间:2019-03-15 16:34:16

标签: c# asp.net asp.net-core moq razor-pages

我正在尝试模拟一个注册用户进行测试的类。在测试代​​码中,我可以明显看到它在下面的 callbackUrl 上失败了。

PageModel类具有一个声明为Url的字段IUrlHelper。 IUrlHelper界面有5种方法,包括.Page()。模拟这5个将很容易,但是我不知道如何模拟扩展方法。

有人可以帮忙吗?我已经坚持了好多年了。

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();
    }

2 个答案:

答案 0 :(得分:1)

简而言之,您不能模拟扩展方法,因为它们是静态方法,moq仅能处理对象。但是this post会告诉您更多信息。

您可能需要更改测试方式,将其移至控制器级别。但是我认为有一个技术解决方案。我们可以做到,但也许我们不应该这样做。

但是,您应该能够填充该方法。它正在与另一个交换方法地址。确保您确实需要它,并确保它仅在测试中使用。

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;
    }
}

用法:

ShimHelper.Replace<ExtensionClass, MockedExtensionClass>();

您的模拟扩展类在哪里与方法签名完全匹配。在您的测试装置设置中运行此程序,您应该会很好。

答案 1 :(得分:0)

我尝试了ICodeGorilla的解决方案,但发现静态类型不能用作类型参数。因此,我对此进行了一些修改:

        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();
        }

现在的用法是:

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

我发现这对于MVC中的AjaxRequestExtensions非常有效。