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);
}
Zak
You are a LIFESAVER. I just spent several hours trying to work out how to mock one of these damned things. THANK YOU!