【问题标题】:ASP.NET MVC Controller Unit Testing - Problem with UrlHelper ExtensionASP.NET MVC 控制器单元测试 - UrlHelper 扩展问题
【发布时间】:2011-08-22 01:43:51
【问题描述】:

尝试在我的 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 上下文,包括服务器变量、控制器上下文、会话等。到目前为止一切顺利。

直到我在我的操作方法中达到这一行:

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

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

【问题讨论】:

  • 我发现 UrlHelper 很难模拟,至少在 Rhino Mocks 中是这样。我有一个解决方案,但它不是特别优雅,基本上是伪造而不是嘲笑。我有兴趣看看你得到什么答案。如果你没有得到任何东西,请告诉我,我明天会发布一些代码——我现在无法访问它。使用扩展方法会使事情变得复杂,因为您需要有一个帮助器实例来工作,并且您不能将一个实例注入到您的扩展中。
  • 你看过这个问题吗?上面有很多肉:stackoverflow.com/questions/674458/…
  • @Matt Greer - 看起来很有用。但同样,我正在使用“区域” - 尝试从我的单元测试中调用该方法时出现错误。我应该把它放在问题中 - 现在编辑......

标签: c# asp.net-mvc unit-testing moq asp.net-mvc-routing


【解决方案1】:

UrlHelper 有一个 constructor,它将 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:你不能从你的测试中调用MvcApplication.RegisterRoutes(routes);?我认为你可以,但如果你不能,你可以考虑实现一个接口来定义你的应用程序启动方法并以这种方式调用它。如果您愿意,我可以提供更多示例代码。
  • 这条特定路线在一个区域内。所以我尝试在MvcApplication.RegisterRoutes(routes); 之前调用AreaRegistration.RegisterAllAreas(); - 但我收到错误消息“在应用程序预启动初始化阶段无法调用此方法。”
  • @RPM1984:啊,好的。我实际上并没有在领域工作那么多,但我会尝试用一些有用的信息来扩展我的答案。
【解决方案2】:

因为这是一个测试,您的测试套件很可能没有像您预期的那样读取 Global.asax。

要解决这个问题,请按照@ataddeini 的建议进行操作并创建一个路由集合。在这个集合中,添加一个新的路由,看起来像......

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

【讨论】:

    【解决方案3】:

    通过模拟 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 助手,它创建所有模拟内容、服务器变量、会话等。

    【讨论】:

    • 我还使用了 Scott Hanselman here 提供的起订量助手。
    • 自 RTM 以来,UrlHelper 现在需要在构造函数中添加一个 HttpRequestMessage,因此我们又一次陷入困境。您能否更新您的回复以反映这一变化?
    • @cadessi - 如果您指的是 MVC 4,我还没有升级 - 但我会在接下来的几周内升级。一旦我这样做,我会更新。 :)
    • @cadessi,我们使用了 MVC 4 和 5,在 UrlHelper 构造函数中不需要 HttpRequestMessage。