在ASP.NET MVC中对控制器进行单元测试是否有真正的价值?


33

我希望这个问题能提供一些有趣的答案,因为这是困扰我一段时间的问题。

在ASP.NET MVC中对控制器进行单元测试是否有真正的价值?

我的意思是,在大多数情况下(而且我不是天才),我的控制器方法即使在最复杂的情​​况下也是如此:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

大多数繁重的工作都是通过MVC管道或我的服务库完成的。

所以也许要问的问题可能是:

  • 对该方法进行单元测试的价值是什么?
  • 它不会中断Request.UserHostAddressModelState带有NullReferenceException吗?我应该试着嘲笑这些吗?
  • 如果我将此方法重新定义为可重复使用的“帮助程序”(考虑到我做了多少次,我可能应该这样做),那么在我真正要测试的大部分都是“管道”时,是否值得进行测试?大概已经被Microsoft测试到寿命不到一英寸了吗?

我认为我的意思确实是,执行以下操作似乎毫无意义,而且是错误的

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

显然,我对这个夸张的,毫无意义的示例感到困惑,但是有人在这里添加任何智慧吗?

期待它...谢谢。


我认为,除非您有无穷的时间和金钱,否则针对特定测试的投资回报率是不值得的。我会写一些凯文指出的测试,以检查更有可能损坏的东西,或者可以帮助您放心地重构某些东西,或者确保按预期进行错误传播。如果需要的话,可以在更全局/基础结构级别进行管道测试,而在单独的方法级别将没有什么价值。不是说它们没有价值,而是“很小”。因此,如果您的情况下提供了良好的投资回报率,则可以继续尝试,否则,请先钓大鱼!
Mrchief

Answers:


18

即使简单,单元测试也有多种用途

  1. 信心,所写的符合预期的输出。验证返回正确的视图似乎很琐碎,但结果是客观证据表明已满足要求
  2. 回归测试。如果需要更改Create方法,则仍然需要对预期输出进行单元测试。是的,输出可能会随之变化,从而导致测试变脆,但这仍然是对不受管理的变更控制的检查

对于该特定操作,我将测试以下内容

  1. 如果_myService为null会怎样?
  2. 如果_myService.Create抛出异常,会抛出特定异常来处理该怎么办?
  3. 成功的_myService.Create是否会返回_Success视图?
  4. 错误会传播到ModelState吗?

您指出了为NullReferenceException检查Request和Model,我认为ModelState.IsValid将负责处理Model的NullReference。

模拟请求可以防止空请求,我认为这通常在生产中是不可能的,但在单元测试中可能会发生。在集成测试中,它将允许您提供不同的UserHostAddress值(就控件而言,请求仍然是用户输入,应进行相应的测试)


嗨,凯文,谢谢您抽出宝贵的时间回答。我要花一会儿时间,看看是否有其他人带来任何东西,但到目前为止,您的想法是最合逻辑/最清楚的。
LiverpoolsNumber13年9

太棒了。很高兴为您提供帮助。
凯文

3

我的控制器也很小。控制器中的大多数“逻辑”都是使用过滤器属性(内置和手写)处理的。因此,我的控制器通常只有少量工作:

  • 根据HTTP查询字符串,表单值等创建模型。
  • 执行一些基本验证
  • 调用我的数据或业务层
  • 产生一个 ActionResult

大多数模型绑定由ASP.NET MVC自动完成。DataAnnotations也为我处理了大部分验证。

即使测试很少,我仍然通常会编写它们。基本上,我测试是否调用了我的存储库并ActionResult返回了正确的类型。我有一个方便的方法来ViewResult确保返回正确的视图路径,并且视图模型看起来像我期望的那样。我还有另一项检查设置的正确控制器/操作RedirectToActionResult。我还有其他测试JsonResult,等等,等等。

对该Controller类进行子类化的不幸结果是,它提供了许多在HttpContext内部使用的便捷方法。这使得很难对控制器进行单元测试。因此,我通常将HttpContext依赖调用放在一个接口后面,并将该接口传递给控制器​​的构造函数(我使用Ninject Web扩展为我创建控制器)。通常,我在该界面上粘贴帮助程序属性,以访问会话,配置设置,IPrinciple和URL帮助程序。

这需要大量的尽职调查,但我认为这是值得的。


感谢您抽出宝贵的时间回答问题,但马上有2个问题。首先,单元测试中的“辅助方法”是危险的。其次,“测试我的存储库是否被调用”-您是说通过依赖注入?
LiverpoolsNumber13年9

为什么便利方法会很危险?我有一BaseControllerTests堂课,他们都住在那里。我嘲笑我的存储库。我使用Ninject将其插入。
特拉维斯公园公园

如果您在助手中犯了错误或错误的假设,会发生什么?我的另一点是,只有集成测试(即端到端)才能“测试”是否调用了存储库。在单元测试中,您还是要手动“更新”或模拟存储库。
LiverpoolsNumber13年9

您将存储库传递给构造函数。您在测试期间对其进行了嘲笑。您确保模拟已按预期执行。佣工只是解构ActionResults到检查过的网址,模型等
特拉维斯公园

好吧,很公平-我稍微误解了您所说的“测试我的存储库被称为”的含义。
LiverpoolsNumber9年9

2

显然,某些控制器比这要复杂得多,但完全基于您的示例:

如果myService引发异常会怎样?

作为旁注。

另外,我会质疑通过引用传递列表的智慧(因为c#无论如何都是通过引用传递,即使不是,也没有必要)-传递errorAction动作(Action),然后服务可以使用该动作将错误消息发送到然后可以根据需要进行处理(也许想将其添加到列表中,也许想添加模型错误,也许要记录它)。

在您的示例中:

例如,执行(string s)=> ModelState.AddModelError(“”,s)代替ref错误。


值得一提的是,这确实假定您的服务位于同一应用程序中,否则序列化问题将发挥作用。
迈克尔

该服务将在单独的dll中。但是无论如何,您可能是正确的“参考”。另一方面,myService是否引发异常也无关紧要。我没有测试myService-我将分别测试其中的方法。我说的是(可能)模拟myService对ActionResult“单元”进行纯测试。
LiverpoolsNumber13年9

您的服务和控制器之间是否有1:1映射?如果不是,某些控制器是否使用多个服务调用?如果是这样,您可以测试这些交互?
迈克尔

否。在一天结束时,服务方法接受输入(通常是视图模型,甚至只是字符串/整数),它们“执行任务”,然后返回布尔值/错误(如果为false)。控制器和服务层之间没有“直接”链接。完全分开。
LiverpoolsNumber13年9

是的,我了解到,我正在尝试了解控制器与服务层之间的关系模型-假设每个控制器没有相应的服务方法,则可以推断某些控制器可能需要使用不止一种服务方法?
迈克尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.