【问题标题】:How to mock extension methods with Moq | Url.Page()如何使用 Moq 模拟扩展方法 |网址.Page()
【发布时间】:2019-08-06 19:03:15
【问题描述】:

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

PageModel 类有一个字段 IUrlHelper 声明为 Url。 IUrlHelper 接口有 5 个方法,NOT 包括 .Page()。模拟这 5 个很容易,但我不知道如何模拟扩展方法。

有人可以帮忙吗?多年来我一直坚持这一点。

注册模型

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

注册模型测试

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

【问题讨论】:

  • 1) 您将无法有效地对 Razor 页面进行单元测试。您应该使用测试服务器运行集成测试。 2) Url.Page 失败,因为没有 HttpContext。没有必要模拟它,因为返回是一致的,你只需要确保有一个HttpContext。因此需要进行集成测试。
  • 我同意,我在下面发布了一个选项,但这更像是最后的版本。在这种情况下,我会问自己我在测试什么,感觉就像你在测试 dotnet 库而不是你的代码。
  • @ChrisPratt @ICodeGorilla 就HttpContext 而言,这篇文章会解决这个问题吗? docs.microsoft.com/en-us/aspnet/core/test/… 。或者你会说集成测试仍然是要走的路?
  • 这就是为什么我稍微整理了一下我的陈述。如果您模拟所有依赖项,则可以进行单元测试,但是有很多,并且在某个时刻,您比实际代码正确模拟所有内容的测试更多。诸如 Razor 页面或控制器操作之类的东西实际上是事物的系统,进行集成测试更有意义。
  • @ICodeGorilla 谢谢我试试看!

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


【解决方案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>();

您的模拟扩展类与方法签名完全匹配。在您的测试夹具设置中运行它,您应该会很好。

【讨论】:

    【解决方案2】:

    我尝试了 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 非常有效。

    【讨论】:

      猜你喜欢
      • 2010-10-08
      • 2011-01-18
      • 2017-10-23
      • 1970-01-01
      • 1970-01-01
      • 2011-04-17
      • 1970-01-01
      相关资源
      最近更新 更多