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

我正在寻找关于.NET mvc控制器的有效unit testing的build议。

在我工作的地方,许多这样的testing使用moq来模拟数据层并声明某些数据层方法被调用。 这对我来说似乎并不有用,因为它从本质上validation了实现没有改变,而不是testingAPI。

我也读过文章推荐的东西,如检查返回的视图模型的types是正确的。 我可以看到提供了一些价值,但是单独看起来似乎不值得编写许多模拟代码(我们的应用程序的数据模型非常庞大和复杂)。

任何人都可以提出一些更好的方法来控制器unit testing或解释为什么上述方法是有效/有用的?

谢谢!

控制器unit testing应该在您的操作方法中testing代码algorithm,而不是在数据层中。 这是模拟这些数据服务的一个原因。 控制器期望从存储库/服务/等接收某些值,并在从它们接收到不同的信息时采取不同的行为。

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

检查返回的视图模型的types是有价值的,因为如果返回错误types的视图模型,MVC将抛出一个运行时exception。 您可以通过运行unit testing来防止这种情况发生。 如果testing失败,则视图可能会在生产中抛出exception。

unit testing可以是有价值的,因为它们使重构变得更容易。 您可以更改实现,并确保所有unit testing都通过,但行为仍然相同。

回答评论#1

如果更改被测方法的实现需要更改/删除较低层的模拟方法,那么unit testing也必须更改。 但是,这不应该像你想像的那样频繁发生。

典型的红绿重构工作stream程要求在编写testing方法之前编写unit testing。 (这意味着在短时间内,您的testing代码将无法编译,这就是为什么许多年轻/缺乏经验的开发人员难以采用红色绿色重构。)

如果你先写unit testing,你会知道控制器需要从底层获取信息。 你怎么能确定它试图获得这些信息? 通过嘲笑提供信息的低层方法,并断言控制器调用低层方法。

当我使用术语“改变实施”时,我可能会错过。 当一个控制器的动作方法和相应的unit testing必须被改变,以改变或移除一个模拟的方法时,你确实正在改变控制器的行为。 根据定义,重构意味着改变实现而不改变整体行为和预期结果。

红绿重构是一种质量保证方法,有助于防止代码出现之前的错误和缺陷。 通常情况下,开发人员会更改实现方式,以便在出现错误后删除它 所以要重申,你所担心的事情不应该像你想象的那样频繁发生。

你应该首先把你的控制器饮食。 然后你可以玩unit testing它们。 如果他们很胖,而且你把所有的业务逻辑都塞进了他们的内部,那么我同意你会在你的unit testing中把你的生活嘲弄一下,抱怨这是浪费时间。

当你谈论复杂的逻辑时,这并不一定意味着这个逻辑不能在不同的层次上分开,每个方法都要单独进行unit testing。

unit testing的重点是根据一组条件来独立testing一个方法的行为。 你使用mock设置testing的条件,并通过检查它与周围的其他代码的交互来断言该方法的行为 – 通过检查它试图调用哪些外部方法,特别是通过检查给定条件返回的值。

因此,在返回ActionResults的Controller方法的情况下,检查返回的ActionResult的值非常有用。

看看“为控制器创buildunit testing”一节, 这里使用了Moq的一些非常清晰的例子 。

这里是一个很好的示例,它testing当Controller尝试创build联系人logging并返回失败时返回一个合适的视图。

[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); } 

在unit testing控制器中我看不出多less点,因为它通常只是连接其他部分的一段代码。 unit testing通常包括大量的嘲弄,只是validation其他服务连接正确。 testing本身是实现代码的反映。

我更喜欢集成testing – 我没有开始使用具体的控制器,而是使用Url,并validation返回的模型是否具有正确的值。 在Ivonna的帮助下,testing可能如下所示:

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

我可以模拟数据库访问,但我更喜欢不同的方法:设置SQLite的内存中的实例,并与每个新的testing重新创build它,连同所需的数据。 它使得我的testing足够快,但是不是复杂的嘲弄,而是让它们清楚,例如只是创build并保存一个User实例,而不是模拟UserService (这可能是一个实现细节)。

是的,你应该一路testing数据库。 你嘲笑的时间less了,你从嘲笑中得到的价值也不那么严重(系统中80%的错误不能被嘲笑所挑剔)。

当你从控制器到数据库或者networking服务一路testing的时候,它不是所谓的unit testing,而是集成testing。 我个人相信集成testing,而不是unit testing。 并且能够成功地进行testing驱动开发。

这是它如何为我们的团队工作。 在开始的每个testing类都会重新生成DB,并用最less的数据集(例如:用户angular色)填充/种子表。 基于控制器的需求,我们填充数据库并validation控制器是否完成任务。 这样devise的方式使得其他方法留下的数据库损坏的数据永远不会使testing失败。 除了时间之外,几乎所有的unit testing(尽pipe它是一个理论)都是可以得到的。

当我被迫使用模拟/存根时,在我的职业生涯中只有2%(或很less)的情况,因为不可能创build一个更现实的数据源。 但在所有其他情况下,集成testing是一种可能性。

我们花了时间用这种方法达到成熟的水平。 我们有一个很好的框架来处理testing数据的填充和检索(头等公民)。 而且它付出了很大的时间:)。 第一步是说再见嘲笑和unit testing。 如果嘲笑没有道理,那么他们不适合你! 整合testing让你睡得很好

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

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

集成testing或functiontesting必须直接处理DB。 没有嘲笑。 所以这些是步骤。 你想testinggetEmployee() 。 所有这5个步骤都是在一个testing方法中完成的。

  1. 丢弃数据库
  2. 创build数据库并填充angular色和其他数据
  3. 使用ID创build员工logging
  4. 使用此ID并调用getEmployee()
  5. 现在断言()/validation返回的数据是否正确

    这certificate了getEmployee()的作品。 直到3的步骤要求你只有testing项目使用的代码。 第4步调用应用程序代码。 我的意思是创build一个员工(第2步)应该通过testing项目代码而不是应用程序代码来完成。 如果有一个应用程序代码来创build员工(例如: CreateEmployee() ),那么不应该使用这个。 同样的方法,当我们testingCreateEmployee()时,不应该使用GetEmployee()应用程序代码。 我们应该有testing项目代码从表中获取数据。

这样就没有嘲笑! 删除和创build数据库的原因是为了防止数据库中有错误的数据。 用我们的方法,无论我们运行多less次,testing都会通过。

特别提示:在步骤5中,如果getEmployee()返回一个员工对象。 如果以后的开发人员删除或更改字段名,testing会因为字段validation而中断。 如果开发者以后再添加一个新字段呢? 他/她忘记添加一个testing(断言)? 解决方法是始终添加一个字段计数检查。 例如:Employee对象有4个字段(名字,姓氏,指定,性别)。 所以员工对象的Assert字段数是4。 而且我们的testing会因计数而失败,并提醒开发人员为新添加的字段添加断言字段。 而且我们的testing代码会将这个新的字段添加到数据库并检索并validation。

通常当你谈论unit testing时,你正在testing一个单独的过程或方法,而不是整个系统,而试图消除所有的外部依赖。

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