一个单元应该如何测试.NET MVC控制器?


72

我正在寻找有关.NET mvc控制器的有效单元测试的建议。

在我工作的地方,许多此类测试都使用moq来模拟数据层并断言某些数据层方法已被调用。这对我来说似乎没有用,因为它实质上是在验证实现没有更改,而不是测试API。

我还阅读了一些推荐文章,例如检查返回的视图模型的类型是否正确。我可以看到它提供了一些价值,但仅凭它似乎不值得编写许多行模拟代码(我们的应用程序的数据模型非常大而复杂)。

谁能提出一些更好的控制器单元测试方法或解释上述方法为何有效/有用?

谢谢!

Answers:


54

控制器单元测试应该在您的操作方法中而不是在数据层中测试代码算法。这是模拟这些数据服务的原因之一。控制器希望从存储库/服务/等中接收某些值,并且当从存储库/服务/中接收到不同的信息时,控制器将采取不同的行动。

您编写单元测试来断言控制器在非常特定的场景/情况下以非常特定的方式运行。您的数据层是应用程序的一部分,可将这些情况提供给控制器/操作方法。断言控制器调用了一种服务方法很有价值,因为您可以确定控制器从另一个地方获取信息。

检查返回的视图模型的类型很有价值,因为如果返回了错误的视图模型类型,则MVC将抛出运行时异常。您可以通过运行单元测试来防止这种情况在生产中发生。如果测试失败,则视图可能会在生产中引发异常。

单元测试可能很有价值,因为它们使重构更加容易。您可以更改实现,并通过确保所有单元测试都通过来断言行为仍然相同。

回答评论1

如果更改被测方法的实现要求更改/删除下层模拟方法,则单元测试也必须更改。但是,这不应该像您想象的那样频繁发生。

典型的红绿重构工作流程要求在编写单元测试之前编写单元测试。(这意味着在很短的时间内,您的测试代码将无法编译,这就是为什么许多年轻/没有经验的开发人员难以采用红色绿色重构的原因。)

如果您首先编写单元测试,那么您将知道控制器需要从较低层获取信息。您如何确定它试图获取该信息?通过模拟提供信息的下层方法,并断言下层方法由控制器调用。

当我使用术语“更改实现”时,我可能会误会。当必须更改控制器的操作方法和相应的单元测试以更改或删除模拟方法时,您实际上是在更改控制器的行为。根据定义,重构意味着在不改变整体行为和预期结果的情况下更改实现。

红绿重构是一种质量保证方法,可以帮助防止代码中的错误和缺陷出现之前。通常,开发人员会更改实现以在错误出现后删除它们。因此,重申一下,您担心的情况不会像您想的那样频繁发生。


我同意您的大部分意见,但我不理解如果单元测试需要实现调用某些较低级别的方法(您的第二点),那么在更改实现时(最后一点),单元测试将如何提供帮助。
ChaseMedallion 2012年

我仍然完全不同意测试一个函数调用另一个函数的想法,但是当您从“测试优先”的角度考虑它时,我了解您来自何处。同样,您可能是对的,在许多情况下,服务调用的更改确实表示控制器行为的更改。感谢您的周到,描述性和合理性的答复!
ChaseMedallion 2012年

27

您首先应该节制饮食。然后,您可以它们进行有趣的单元测试。如果它们很胖,并且您已经将所有业务逻辑塞入其中,那么我同意您将在单元测试中传递生活中的嘲笑内容,并抱怨这是在浪费时间。

当您谈论复杂的逻辑时,这不一定意味着该逻辑不能在不同的层中分离,并且每种方法都必须进行隔离的单元测试。


9
链接使您的控制器节食了:(
Erik Philips

2
我认为这是达林指向的链接: youtube.com/watch?
v=8TYJjRxXnXs

我以为我会编辑并更改指向您的链接,很棒的工作@ryanulit!
巴特

10

是的,您应该一直测试到数据库。您投入模拟的时间更少了,并且从模拟中获得的价值也非常少(模拟中无法选择系统中80%的可能错误)。

从控制器到数据库或Web服务的全过程测试时,这不是单元测试,而是集成测试。我个人认为集成测试与单元测试相对(即使它们都有不同的用途)。而且我能够通过集成测试(场景测试)成功地进行测试驱动的开发。

这是我们团队的工作方式。开始时的每个测试类都会重新生成数据库,并使用最少的数据集(例如:用户角色)填充/填充表。根据控制器的需求,我们填充数据库并验证控制器是否完成了任务。这样设计的方式是,其他方法遗留的DB损坏数据将永远不会通过测试。除了花费时间之外,几乎所有单元测试的质量(即使是理论上的)都是可以获取的。使用容器可以减少顺序运行所需的时间。同样对于容器,我们不需要重新创建数据库,因为每个测试都会在容器中获得自己的新数据库(在测试之后将被删除)。

在我的职业生涯中,只有2%的情况(或很少)是因为无法创建更真实的数据源而被迫使用模拟/存根。但是在所有其他情况下,也可以进行集成测试。

通过这种方法,我们花了一些时间才能达到成熟的水平。我们有一个很好的框架来处理测试数据的填充和检索(头等公民)。而且还可以赚大钱!第一步是告别模拟和单元测试。如果模拟没有意义,那么它们不适合您!集成测试为您提供良好的睡眠。

==================================

在下面的评论后编辑:演示

集成测试或功能测试必须直接处理数据库/源。没有嘲笑。这些就是步骤。您要测试getEmployee(emp_id)。以下所有这5个步骤都是通过单一测试方法完成的。

  1. 删除数据库

  2. 创建数据库并填充角色和其他基础数据

  3. 创建具有ID的员工记录

  4. 使用此ID并调用getEmployee(emp_id)//这可以是api-url调用(这样,无需在测试项目中维护数据库连接字符串,并且我们可以通过简单地更改域名来测试几乎所有环境)

  5. 现在Assert()/验证返回的数据是否正确

    这证明getEmployee()有效。直到3的步骤要求您仅将代码用于测试项目。步骤4调用应用程序代码。我的意思是创建员工(步骤2)应该由测试项目代码而非应用程序代码完成。如果有用于创建员工的应用程序代码(例如:CreateEmployee()),则不应使用此代码。同样,当我们测试在CreateEmployee() ,然后getEmployee的()不应该用于应用程序代码。我们应该有一个测试项目代码,用于从表中获取数据。

这样就没有模拟了!删除和创建数据库的原因是为了防止DB损坏数据。使用我们的方法,无论我们运行多少次,测试都将通过。

特别提示:在步骤5中,getEmployee()返回一个employee对象。如果以后开发人员删除或更改字段名称,测试将失败。如果开发人员以后添加新字段怎么办?他/她忘记为此添加一个测试(断言)?测试不会进行。解决方案是添加字段计数检查。例如:雇员对象具有4个字段(名字,姓氏,名称,性别)。因此,assert对象的字段的数目为4。因此,当添加新字段时,由于计数,我们的测试将失败,并提醒开发人员为新添加的字段添加断言字段。

这是一篇很棒的文章,讨论了集成测试比单元测试的好处,因为“单元测试必杀!” (它说)


1
我喜欢你的方法。我也更喜欢集成测试,因为它可以显示系统是否正常运行。单元测试似乎只处理逻辑部分,这很好,但对我来说似乎还不够。
Mariusz

最好先对数据库进行单元测试,然后在mvc测试项目中进行简单的集成测试,或者在mvc测试项目中进行一个集成测试,该测试也测试返回的数据?
Muflix '17

2
是。集成测试或功能测试必须直接处理DB。没有嘲笑。我已经用演示步骤编辑了答案。
蓝云

像集成测试方法一样,我也在以类似的方式思考。主要目标是确保进行更改时我们的应用程序不会损坏,而不是使用模拟等编写单元测试。
桑耶夫

1
不管您信不信,关于如何在整个互连网上集成测试控制器的最具体也是唯一的概述之一。谢谢@BlueClouds ...谢谢。答案我写到这里的问题是:stackoverflow.com/questions/53014850/...
凯尔Vassella

9

单元测试的重点是根据一组条件单独测试方法的行为。您可以使用模拟设置测试条件,并通过检查该方法与周围其他代码的交互方式来断言该方法的行为-通过检查它尝试调用的外部方法,特别是通过检查在给定条件下返回的值来确定该方法的行为。

因此,对于返回ActionResults的Controller方法而言,检查返回的ActionResult的值非常有用。

请参阅此处的“为控制器创建单元测试”部分 ,以获取使用Moq的一些非常清晰的示例

这是该页面上的一个很好的示例,该示例测试当Controller尝试创建联系人记录并且失败时返回了适当的视图。

[TestMethod]
public void CreateInvalidContact()
{
    // Arrange
    var contact = new Contact();
    _service.Expect(s => s.CreateContact(contact)).Returns(false);
    var controller = new ContactController(_service.Object);

    // Act
    var result = (ViewResult)controller.Create(contact);

    // Assert
    Assert.AreEqual("Create", result.ViewName);
}

8

我对控制器的单元测试没有多大意义,因为它通常只是连接其他部分的一段代码。单元测试通常包括很多模拟,只是验证其他服务是否正确连接。测试本身是实现代码的反映。

我更喜欢集成测试-我不是从具体的控制器开始,而是从一个Url开始,并验证返回的Model是否具有正确的值。在Ivonna的帮助下,测试可能类似于:

var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);

var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);

我可以模拟数据库访问,但是我更喜欢另一种方法:设置SQLite的内存中实例,并在每次新测试以及所需数据时重新创建它。它使我的测试足够快,但是我没有进行复杂的模拟,而是将它们弄清楚了,例如,仅创建并保存一个User实例,而不是模拟UserService(可能是实现细节)。


3
我认为您误解了单元测试背后的想法。单元测试应该以与其他层/依赖关系等分离的方式测试一段代码(单元)。在您的情况下,您应该针对MVC路由,控制器操作和服务编写单独的测试。您在此处描述的是端到端测试,而SpecFlow和Watin是用于此类测试的良好工具。
Piotr Lewandowski

1
谢谢,我知道。请注意,我写了“我更喜欢集成测试”。例如,WatiN不允许我调查我的模型。
ulu 2014年

“因为它通常只是连接其他部分的一段代码”,我猜您可能对任何一段代码都这么说。
rsenna

1

通常,在谈论单元测试时,您正在测试一个单独的过程或方法,而不是整个系统,同时试图消除所有外部依赖性。

换句话说,在测试控制器时,您正在逐个方法地编写测试,甚至不需要加载视图或模型,这些都是您应该“模拟”的部分。然后,您可以将模拟更改为返回在其他测试中难以重现的值或错误。

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.