ASP.NET MVC 控制器单元测试 - UrlHelper 扩展存在问题

15

我尝试在我的ASP.NET MVC 3 Web应用程序中进行一些控制器单元测试。

我的测试代码如下:

[TestMethod]
public void Ensure_CreateReviewHttpPostAction_RedirectsAppropriately()
{
   // Arrange.
   var newReview = CreateMockReview();

   // Act.
   var result = _controller.Create(newReview) as RedirectResult;

   // Assert.
   Assert.IsNotNull(result, "RedirectResult was not returned");
}
很简单。基本上是测试一个[HttpPost]操作来确保它返回一个RedirectResult(PRG模式)。我没有使用RedirectToRouteResult,因为其中没有任何重载支持锚链接。继续往下看。
现在,我正在使用Moq来模拟Http Context,包括服务器变量、控制器上下文、会话等等。到目前为止一切都很顺利。
直到我在我的操作方法中遇到了这行代码:
return Redirect(Url.LandingPageWithAnchor(someObject.Uri, review.Uri);

LandingPageWithAnchor 是一个自定义的 HTML 帮助器:

public static string LandingPageWithAnchor(this UrlHelper helper, string uri1, string uri2)
{
   const string urlFormat = "{0}#{1}";

   return string.Format(urlFormat,
                helper.RouteUrl("Landing_Page", new { uri = uri1}),
                uri2);
}

基本上,我重定向到另一个页面,这是新内容的“登陆页面”,带有新评论的锚点。很酷。

现在,以前这种方法失败了,因为UrlHelper为空。

所以在我的模拟中,我做了这个:

controller.Url = new UrlHelper(fakeRequestContext);

由于路由表中没有“Landing_Page”的定义,导致其进一步出现问题并失败了。

所以我知道我需要模拟“某些东西”,但我不确定是:

a)路由表
b)UrlHelper.RouteUrl 方法
c)我编写的 UrlHelper.LandingPageWithAnchor 扩展方法

有人能提供一些指导吗?

编辑

这个特定的路由在一个区域中,因此我尝试在我的单元测试中调用该区域的注册:

AreaRegistration.RegisterAllAreas();

但我遇到了一个 InvalidOperationException:

在应用程序的预启动初始化阶段无法调用此方法。


我发现使用Rhino Mocks模拟UrlHelper有些困难。虽然我找到了一个解决方案,但并不十分优雅,基本上是伪造而非模拟。我很想看看你得到的答案。如果你没有得到任何东西,请告诉我,我明天会贴一些代码 - 我现在无法访问。使用扩展方法会使事情变得复杂,因为您需要一个helper实例来工作,而您无法将其注入到扩展中。 - tvanfosson
你看过这个问题吗?它有很多实质性的内容:https://dev59.com/yXRB5IYBdhLWcg3wQVWV - Matt Greer
@Matt Greer - 看起来很有用。但是我正在使用“区域” - 当我尝试从我的单元测试中调用该方法时,我遇到了错误。我应该在问题中加上这一点 - 现在正在编辑... - RPM1984
3个回答

12

通过模拟 HttpContext、RequestContext 和 ControllerContext,注册路由,然后创建一个使用这些路由的 UrlHelper,最终实现了功能。

大致步骤如下:

public static void SetFakeControllerContext(this Controller controller, HttpContextBase httpContextBase)
{
    var httpContext = httpContextBase ?? FakeHttpContext().Object;
    var requestContext = new RequestContext(httpContext, new RouteData());
    var controllerContext = new ControllerContext(requestContext, controller);
    MvcApplication.RegisterRoutes();
    controller.ControllerContext = controllerContext;
    controller.Url = new UrlHelper(requestContext, RouteTable.Routes);
}

FakeHttpContext()是Moq的一个辅助函数,用于创建所有模拟内容,包括服务器变量、会话等。


2
我还使用了Scott Hanselman在这里提供的Moq助手。 - Dan Atkinson
自从RTM以来,UrlHelper现在在构造函数中需要一个HttpRequestMessage,所以我们又回到了起点。你能否更新你的响应以反映这个变化? - Ismael
@cadessi - 如果你说的是MVC 4,我还没有升级 - 但我会在接下来的几周内进行升级。一旦我升级了,我就会更新。 :) - RPM1984
@cadessi,我们使用过MVC 4和5,且在UrlHelper构造函数中不需要HttpRequestMessage。 - ps2goat

0

UrlHelper有一个构造函数,它接受一个RouteCollection作为第二个参数。如果您使用MVC默认设置,则认为这应该适用于您:

var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);

controller.Url = new UrlHelper(fakeRequestContext, routes);

另一种选择是修改应用程序的启动方式,使测试和工作变得更加容易。如果您定义了这样一个接口:

public interface IMvcApplication
{
    void RegisterRoutes(RouteCollection routes);
    // Other startup operations    
}

使用实现:

public class MyCustomApplication : IMvcApplication
{
    public void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        // Your route registrations here
    }

    // Other startup operations
}

然后您可以像这样修改您的Global.asax

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        var app = new MyCustomApplication();
        app.RegisterRoutes(RouteTable.Routes);
        // Other startup calls
    }
}

同时仍然具有灵活性,可以注册您的路由进行测试。就像这样:

private IMvcApplication _app;
private RouteCollection _routes;

[TestInitialize]
public void InitializeTests()
{
    _app = new MyCustomApplication();

    _routes = new RouteCollection();
    _app.RegisterRoutes(_routes);
}

这同样可以被利用来与区域注册一起工作。

private RouteCollection _routes;
private MyCustomAreaRegistration _area;

[TestInitialize]
public void InitTests()
{
    _routes = new RouteCollection();
    var context = new AreaRegistrationContext("MyCustomArea", _routes);

    _area.RegisterArea(context);
}

我看到了,但路由是在global.asax和区域注册中创建的。我没有访问MvcApplication.RegisterRoutes(routes)的权限。我需要“重新创建”虚拟路由条目吗? - RPM1984
@RPM1984:你不能从测试中调用MvcApplication.RegisterRoutes(routes);吗?我认为你应该可以,但如果不能的话,你可以考虑实现一个接口来定义你的应用程序启动方法,并以这种方式调用它。如果你愿意,我可以提供一些更多的示例代码。 - ataddeini
这个特定的路由在一个区域内。所以我试着在MvcApplication.RegisterRoutes(routes);之前调用AreaRegistration.RegisterAllAreas();,但是出现了一个错误,说“此方法无法在应用程序预启动初始化阶段调用”。 - RPM1984
@RPM1984:啊,好的。其实我没怎么接触过区域,但我会尽量扩展我的答案,并提供一些有用的信息。 - ataddeini

0

因为这是一个测试,所以你的测试套件很可能没有像你期望的那样读取 Global.asax 文件。

要解决此问题,请按照 @ataddeini 建议的方法创建路由集合。向该集合中添加一个新路由,它将如下所示...

var routes = new RouteCollection();
routes.Add(new Route("Landing_Page", new { /*pass route params*/}, null));
var helper = new UrlHelper(fakeRequestContext, routes);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接