【问题标题】:TryUpdateModel causing error from unit test cases (Asp.net mvc)TryUpdateModel 导致单元测试用例出错(Asp.net mvc)
【发布时间】:2011-06-15 14:52:13
【问题描述】:

我在控制器中有后期操作。代码如下所示

     [HttpPost]
    public ActionResult Create(Int64 id, FormCollection collection)
    {
        var data = Helper.CreateEmptyApplicationsModel();

        if (TryUpdateModel(data))
        {
              // TODO: Save data
             return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
        }
        else
        {
            // TODO: update of the model has failed, look at the error and pass back to the view
            if (!ModelState.IsValid)
            {
                if (id != 0) Helper.ShowLeftColumn(data, id);
                return View("Create", data);
            }
        }

        return RedirectToAction("Details", "Info", new { area = "Deals", InfoId= id });

    }

我为此编写了如下测试用例

    [TestMethod]
    public void CreateTest_for_post_data()
    {           
        var collection = GetApplicantDataOnly();           
        _controller.ValueProvider = collection.ToValueProvider();
        var actual = _controller.Create(0, collection);
        Assert.IsInstanceOfType(actual, typeof(RedirectToRouteResult));
    }

当我调试这个单个测试用例时,测试用例通过了,因为条件 if (TryUpdateModel(data)) 返回 true 并进入 if 条件。 但是当我从整个解决方案中调试测试用例时,这个测试用例失败了,因为它进入了“if (TryUpdateModel(data))”的其他条件。

我不知道为什么..

请帮忙...

谢谢

【问题讨论】:

    标签: asp.net-mvc-3 unit-testing


    【解决方案1】:

    我遇到过类似的问题,只要您不需要使用FormCollection,它将解决您的问题。

    自从我知道自动绑定功能那天起,我就再也没有使用过TryUpdateModel。简而言之,自动绑定几乎可以完成TryUpdateModel 的工作,也就是说,它会根据在 FormCollection 中找到的值设置模型对象并尝试验证模型。它会自动执行此操作。您所要做的就是在 ActionMethod 中放置一个参数,它会自动使用 FormCollection 中的值填充其属性。然后你的 Action 签名会变成这样:

    public ActionResult Create(Int64 id, SomeModel data)
    

    现在您根本不需要致电TryUpdateModel。您仍然需要检查 ModelState 是否有效来决定是否重定向或返回视图。

    [HttpPost]
    public ActionResult Create(Int64 id, SomeModel data)
    {
        if (ModelState.IsValid)
        {
              // TODO: Save data
             return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
        }
        else
        {
            if (id != 0) Helper.ShowLeftColumn(data, id);
            return View("Create", data);
        }
    }
    

    这不会在您的单元测试中引发异常,因此解决了一个问题。但是,现在还有另一个问题。如果您使用上面的代码运行您的应用程序,它会工作得很好。您的模型将在输入 Action 时进行验证,并返回正确的 ActionResult(重定向或视图)。但是,当您尝试对两条路径进行单元测试时,您会发现模型将始终返回重定向,即使模型无效也是如此。

    问题在于,在进行单元测试时,模型根本没有经过验证。而且由于 ModelState 默认是有效的,ModelState.IsValid 在你的单元测试中总是会返回 true,因此即使模型无效也会总是返回重定向。

    解决方案:调用TryValidateModel 而不是ModelState.IsValid。这将强制您的单元测试验证模型。这种方法的一个问题是,这意味着模型将在您的单元测试中验证一次,在您的应用程序中验证两次。这意味着发现的任何错误都将在您的应用程序中记录两次。这意味着如果您在视图中使用 ValidationSummary 辅助方法,您会看到列出的一些重复消息。

    如果实在无法忍受,可以先清除ModelState,然后再调用TryValidateModel。这样做会有一些问题,因为您会丢失一些有用的数据,例如如果您清除 ModelState 时的尝试值,那么您可以只清除 ModelState 中记录的错误。您可以通过深入挖掘 ModelState 并清除存储在每个项目中的每个错误来做到这一点,如下所示:

    protected void ClearModelStateErrors()
    {
        foreach (var modelState in ModelState.Values)
            modelState.Errors.Clear();
    }
    

    我已将代码放在一个方法中,以便所有操作都可以重用它。我还添加了protected 关键字,以暗示这可能是一个有用的方法,可以放置在所有控制器派生自的 BaseController 中,以便它们都可以访问此方法。

    最终解决方案:

    [HttpPost]
    public ActionResult Create(Int64 id, SomeModel data)
    {
        ClearModelStateErrors();
    
        if (ModelState.IsValid)
        {
              // TODO: Save data
             return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
        }
        else
        {
            if (id != 0) Helper.ShowLeftColumn(data, id);
            return View("Create", data);
        }
    }
    

    注意:我意识到我没有阐明根本问题。那是因为我不完全理解根本问题。如果您注意到单元测试失败,它会失败,因为ArgumentNullException 被抛出,因为ControllerContext 为空,如果ControllerContext 为空,它被传递给抛出异常的方法。 (用他们该死的防御性编程来诅咒 MVC 团队)。

    如果你试图模拟ControllerContext,你仍然会得到一个异常,这次是NullReferenceException。有趣的是,异常的堆栈跟踪显示两个异常都发生在同一个方法上,或者我应该说构造函数,位于System.Web.Mvc.ChildActionValueProvider。我手边没有源代码的副本,所以我不知道是什么导致了异常,而且我还没有找到比我上面提供的解决方案更好的解决方案。我个人不喜欢我的解决方案,因为我正在改变我的应用程序编码方式以利于我的单元测试,但似乎没有更好的选择。我敢打赌,真正的解决方案将涉及模拟其他一些对象,但我只是不知道是什么。

    此外,在任何人得到任何聪明的想法之前,模拟 ValueProvider 不是解决方案。它会停止异常,但您的单元测试不会验证您的模型,并且您的 ModelState 将始终报告模型有效,即使它不是。

    【讨论】:

      【解决方案2】:

      您可能需要稍微清理一下您的代码:

      [HttpPost]
      public ActionResult Create(int id, FormCollection collection)
      {
          var data = Helper.CreateEmptyApplicationsModel();
      
          if (!ModelState.IsValid)
          {
              if (id != 0)
              {
                  Helper.ShowLeftColumn(data, id);
              }
      
              return View("Create", data);
          }
      
          if (TryUpdateModel(data))
          {
              return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
          }
      
          return RedirectToAction("Details", "Info", new { area = "Deals", InfoId= id });
      }
      

      不要使用Int64,只需使用int

      至于你的失败测试,​​我希望你的测试一直失败,因为TryUpdateModel 将返回 false。当您从单元测试运行代码时,控制器的控制器上下文不可用,因此TryUpdateModel 将失败。

      您需要以某种方式伪造/模拟TryUpdateModel,以便它实际上无法正常运行。相反,您“伪造”它以返回真实。以下是一些帮助链接:

      How do I Unit Test Actions without Mocking that use UpdateModel?

      上面的 SO 答案显示了一个使用 RhinoMocks 的示例,这是一个免费的模拟框架。

      或者这个:

      http://www.codecapers.com/post/ASPNET-MVC-Unit-Testing-UpdateModel-and-TryUpdateModel.aspx

      【讨论】:

        【解决方案3】:

        调试您的测试并检查模型状态错误集合,tryupdatemodel 遇到的所有错误都应该存在。

        【讨论】:

          猜你喜欢
          • 2011-05-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多