Problem


In ASP.NET Core, when you use extension methods on UrlHelperExtensions class, it would be difficult to write Mock in unit test. Because Moq doesn't support mocking extension methods.

For example, the following code that I use in my blog is using Url.Page() method:

var callbackUrl = Url.Page("/Index", null, null, Request.Scheme);

But in my unit test, mocking like this will throw exception:

var mockUrlHelper = new Mock<IUrlHelper>(MockBehavior.Strict);
mockUrlHelper.Setup(x => x.Page("/Index", null, null, It.IsAny<string>())).Returns("callbackUrl").Verifiable();

System.NotSupportedException : Unsupported expression: x => x.Page("/Index", null, null, It.IsAny<string>())
    Extension methods (here: UrlHelperExtensions.Page) may not be used in setup / verification expressions.

Solution


We need to mock the underlying method that the extension method calls. In my case, it is Microsoft.AspNetCore.Mvc.IUrlHelper.RouteUrl(UrlRouteContext routeContext)

How did I found this solution? It's easy, just take a look .NET Core source code here https://source.dot.net/ and you will find how Microsoft test UrlHelperExtensions

Borrow a few code from Microsoft

private Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
{
    context ??= GetActionContextForPage("/Page");

    var urlHelper = _mockRepository.Create<IUrlHelper>();
    urlHelper.SetupGet(h => h.ActionContext)
        .Returns(context);
    return urlHelper;
}

private static ActionContext GetActionContextForPage(string page)
{
    return new()
    {
        ActionDescriptor = new()
        {
            RouteValues = new Dictionary<string, string>
            {
                { "page", page },
            }
        },
        RouteData = new()
        {
            Values =
            {
                [ "page" ] = page
            }
        }
    };
}

Use it in my unit test

var mockUrlHelper = CreateMockUrlHelper();
mockUrlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
    .Returns("callbackUrl");

The mock can run perfectly now!

Complete unit test method for reference:

[Test]
public async Task SignOutAAD()
{
    _mockOptions.Setup(m => m.Value).Returns(new AuthenticationSettings
    {
        Provider = AuthenticationProvider.AzureAD
    });

    var mockUrlHelper = CreateMockUrlHelper();
    mockUrlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
        .Returns("callbackUrl");

    var ctx = new DefaultHttpContext();
    var ctl = CreateAuthController();
    ctl.ControllerContext = new() { HttpContext = ctx };
    ctl.Url = mockUrlHelper.Object;

    var result = await ctl.SignOut();
    Assert.IsInstanceOf(typeof(SignOutResult), result);
}