如何在不使用模拟的情况下对使用UpdateModel的操作进行单元测试?

8
我一直在阅读Scott Guthrie的ASP.NET MVC Beta 1,其中展示了对UpdateModel方法的改进以及它们如何提高单元测试效率。我已经创建了一个类似的项目,但是每当运行包含对UpdateModel调用的UnitTest时,都会收到一个ArgumentNullException,指定了controllerContext参数。
下面是相关部分,从我的模型开始:
public class Country {
  public Int32 ID { get; set; }
  public String Name { get; set; }
  public String Iso3166 { get; set; }
}

控制器操作:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Int32 id, FormCollection form)
{
  using ( ModelBindingDataContext db = new ModelBindingDataContext() ) {
    Country country = db.Countries.Where(c => c.CountryID == id).SingleOrDefault();

    try {
      UpdateModel(country, form);

      db.SubmitChanges();

      return RedirectToAction("Index");
    }
    catch {
      return View(country);
    }
  }
}

最后是我的失败的单元测试:

[TestMethod]
public void Edit()
{
  CountryController controller = new CountryController();
  FormCollection form = new FormCollection();
  form.Add("Name", "Canada");
  form.Add("Iso3166", "CA");

  var result = controller.Edit(2 /*Canada*/, form) as RedirectToRouteResult;

  Assert.IsNotNull(result, "Expected to be redirected on successful POST.");
  Assert.AreEqual("Show", result.RouteName, "Expected to redirect to the View action.");
}

“ArgumentNullException”是由调用“UpdateModel”时抛出的,错误消息为“值不能为 null。参数名: controllerContext”。我认为,在执行测试期间缺少了需要的“System.Web.Mvc.ControllerContext”,因此在某处需要“UpdateModel”。
我还假设自己在某个地方做错了什么,只需要指出正确方向即可。
请帮忙!
3个回答

5
我不认为这是可行的,因为TryUpdateModel(UpdateModel使用的方法)引用了ControllerContext,而在单元测试中调用时ControllerContext为null。我使用RhinoMocks来模拟或存根控制器所需的各个组件。
var routeData = new RouteData();
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
FormCollection formParameters = new FormCollection();

EventController controller = new EventController();
ControllerContext controllerContext = 
    MockRepository.GenerateStub<ControllerContext>( httpContext,
                                                    routeData,
                                                    controller );
controller.ControllerContext = controllerContext;

ViewResult result = controller.Create( formParameters ) as ViewResult;

Assert.AreEqual( "Event", result.Values["controller"] );
Assert.AreEqual( "Show", result.Values["action"] );
Assert.AreEqual( 0, result.Values["id"] );

以下是来自www.codeplex.com/aspnet上Controller.cs源代码的相关部分:

protected internal bool TryUpdateModel<TModel>( ... ) where TModel : class
{

     ....

    ModelBindingContext bindingContext =
           new ModelBindingContext( ControllerContext,
                                    valueProvider,
                                    typeof(TModel),
                                    prefix,
                                    () => model,
                                    ModelState,
                                    propertyFilter );

     ...
}

1
我同意,我可以通过模拟来解决这个问题,但这明显违反了Scott在他的文章中关于UpdateModel示例的说法:“我们不必模拟任何东西来单元测试上述两种表单提交场景。” - Doug Wilson
我查看了TryUpdateModel的源代码(UpdateModel使用它),它确实使用ControllerContext。我已经更新了我的响应,并附上了相关的源代码片段。 - tvanfosson
@Hellfire,我在博客文章的评论中看到了一个评论,表示至少还有一个人遇到了相同的错误。这可能是由于ModelBindingContext在Beta 1发布之前发生了更改。 - tvanfosson
@Tim,谢谢。我也阅读了源代码,希望你是对的。如果它以这种方式发布,那将是一件真正的遗憾。 - Doug Wilson
@Craig,你有任何证实这一点的来源(博客、文章等)吗?我猜我可以按照TV的建议来模拟它,但显然这并不理想。 - Doug Wilson
显示剩余2条评论

3

我遇到了同样的问题。在阅读了tvanfosson的解决方案后,我尝试了一个简单的解决方案,而不涉及模拟框架。

将默认的ControllerContext添加到控制器中,如下所示:

CountryController controller = new CountryController();
controller.ControllerContext = new ControllerContext();

在我的单元测试中,这个方法成功地解决了错误。希望这能帮助到其他人。


嗨,布莱恩。我应该在控制器的哪个位置添加这些语句? - suvenk

0

或者您可以创建表单数据代理,例如

public class CountryEdit {
  public String Name { get; set; }
  public String Iso3166 { get; set; }
}
  • 优点:轻松创建单元测试
  • 优点:定义从post更新的字段白名单
  • 优点:轻松设置验证规则,易于测试。
  • 缺点:您应该将日期从代理移动到模型中

因此,Controller.Action 应该看起来像:

public ActionResult Edit(Int32 id, CountryEdit input)
{
  var Country = input.ToDb();
  // Continue your code
}

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