TDD - 顶级功能有太多的模拟。我是否应该费心去测试呢?

时间:2010-01-14 20:03:24

标签: unit-testing tdd mocking

我有一个带有Web前端,WCF Windows服务后端的.NET应用程序。应用程序非常简单 - 需要一些用户输入,将其发送到服务。该服务执行此操作 - 获取输入(Excel电子表格),提取数据项,检查SQL DB以确保项目尚不存在 - 如果它们不存在,我们向第三方数据供应商发出实时请求并检索结果,将它们插入数据库。它沿途做了一些记录。

我有一个带有单个公共ctor和公共Run()方法的Job类。 ctor接受所有参数,Run()方法执行上述所有逻辑。每个逻辑功能部分被拆分为一个单独的类 - IParser执行文件解析,IConnection执行与数据供应商的交互,IDataAccess执行数据访问等.Job类具有这些接口的私有实例,并使用DI构建默认情况下实际实现,但允许类用户注入任何接口。

在实际代码中,我使用默认的ctor。在我对Run()方法的单元测试中,我使用通过NMock2.0创建的所有模拟对象。这个Run()方法本质上是这个应用程序的“顶级”功能。

现在这是我的问题/问题:这个Run()方法的单元测试很疯狂。我有三个模拟对象,我发送到ctor,每个模拟对象设置自己的期望。最后,我验证。我有一些Run方法可以采用的不同流程,每个流程都有自己的测试 - 它可以找到数据库中的所有内容,而不是向供应商发出请求......或者可以抛出异常并且作业状态可以被设置为'失败'...或者我们可以得到我们没有数据并且需要提出供应商请求的情况(因此需要进行所有这些函数调用)。

现在 - 在你对我大吼大叫之前说'你的Run()方法太复杂了!' - 这个Run方法只有50行代码! (它确实调用了一些私有函数;但整个类只有160行)。由于所有“真实”逻辑都是在此类声明的接口中完成的。 然而,这个函数的最大单元测试是80行代码,有13个调用Expect.BLAH().. _

这使得重新考虑了巨大的痛苦。如果我想改变这个Run()方法,我必须编辑我的三个单元测试并添加/删除/更新Expect()调用。当我需要重构时,我最终花费更多时间创建模拟调用,而不是实际编写新代码。在这个功能上做真正的TDD使得它变得更加困难,如果不是不可能的话。这让我觉得根本不值得对这个顶级函数进行单元测试,因为实际上这个类没有做太多逻辑,它只是将数据传递给它的复合对象(它们都是完全单元测试的,不需要嘲笑)。

那么 - 我是否应该费心去测试这个高级功能呢?这样做我获得了什么?或者我在这里完全滥用mock / stub对象?也许我应该废弃这个类的单元测试,而只是进行自动化集成测试,它使用对象的实际实现和Asserts()对SQL查询来确保存在正确的最终状态数据?我在这里缺少什么?

编辑:Here's the code - 第一个函数是实际的Run()方法 - 然后我的五个测试测试所有五个可能的代码路径。我因NDA原因改了一些,但一般的概念仍然存在。 您对我如何测试此功能有任何疑问,有什么建议可以改变以使其更好? 谢谢。

8 个答案:

答案 0 :(得分:4)

我想我的建议回应了这里发布的大部分内容。

听起来好像你的Run方法需要更多分解。如果它的设计迫使你进入比它更复杂的测试,那就错了。记住这是我们正在谈论的TDD,所以你的测试应该决定你的日常设计。如果这意味着测试私有功能,那就这样吧。没有技术哲学或方法论应该如此严格,以至于你无法做出正确的事情。

此外,我同意其他一些海报,你的测试应该细分为更小的部分。问问自己,如果你是第一次写这个应用程序并且你的Run函数还不存在,你的测试会是什么样子?那回应可能不是你现在的回答(否则你不会问这个问题)。 :)

你所拥有的一个好处是课堂上没有很多代码,所以重构它应该不会很痛苦。

修改

刚看到你发布了代码并有一些想法(没有特别的顺序)。

  1. SyncLock块中的代码太多(IMO)。一般规则是在SyncLock内部将代码保持在最小值。 所有是否必须锁定?
  2. 开始将代码分解为可以独立测试的函数。示例:ForLoop,如果DB中存在ID,则从List(String)中删除ID。有些人可能认为m_dao.BeginJob调用应该是某种可以测试的GetID函数。
  3. 是否可以将任何m_dao程序转换为可以自行测试的功能?我会假设m_dao类在某处有自己的测试,但通过查看代码,可能并非如此。它们应该与m_Parser类中的功能一起使用。这将减轻Run测试的一些负担。
  4. 如果这是我的代码,我的目标是将代码放到一个地方,其中Run内的所有单个过程调用都是自己测试的,Run测试只是测试最终结果。给定输入A,B,C:期望结果X.给出输入E,F,G:期望Y.在其他程序的测试中已经测试了Run到X或Y的详细信息。

    这些只是我的初步想法。我确信可以采取一系列不同的方法。

答案 1 :(得分:2)

两个想法:首先你应该进行集成测试,以确保一切都挂在一起。其次,听起来像你错过了中间物体。在我的世界里,50行是一个很长的方法。如果没有看到代码,很难说更精确。

答案 2 :(得分:1)

我要尝试的第一件事就是重新调整你的单元测试,通过重构一个设置模拟和期望的方法来共享测试之间的设置代码。参数化方法,以便您的期望可配置。您可能需要一个或多个这些设置方法,具体取决于设置从测试到测试的程度。

答案 3 :(得分:1)

  

所以 - 我是否应该费心去测试这个   高级功能?

是。如果有不同的代码路径,那么你应该是。

  

这是怎么做的?   或者我完全误用了mock / stub   对象在这里?

正如J.B.所指出的那样(尼斯在AgileIndia2010上见到你!),推荐Fowler的文章阅读。作为一种粗略的简化:当您不关心协作者返回的值时,使用Stubs。如果你从collaborator.call_method()的返回值改变行为(或者你需要对args进行非平凡的检查,计算返回值),你需要模拟。

建议的重构:

  1. 尝试将模拟的创建和注入移动到常用的Setup方法中。大多数单元测试框架都支持这一点;将在每次测试前调用
  2. 您的LogMessage调用是信标 - 再次呼叫意图揭示方法。例如SubmitBARRequest()。这将缩短您的生产代码。
  3. 尝试将每个Expect.Blah1(..)移动到意图揭示方法中。 这将缩短您的测试代码并使其具有极大的可读性和易于修改。例如 替换所有实例

    Expect.Once.On(mockDao)_             .Method(“BeginJob”)_             .With(New Object(){submittedBy,clientID,runDate,“Sent for Baring”})_             .Will([返回]。价值(0));

  4. ExpectBeginJobOnDAO_AndReturnZero(); // you can name it better

答案 4 :(得分:1)

是否要测试此类功能:您在评论中说了

  

“测试读起来就像实际的一样   功能,因为我使用嘲笑,   它唯一断言的功能是   打电话给派对(我可以检查一下)   这是通过观察50线   功能)“

imho眼球的功能还不够,你没有听说过:“我不敢相信我错过了!” ...你有相当多的场景可能在Run方法中出错,涵盖那个逻辑是一个好主意。

关于测试很脆弱:尝试使用一些共享方法,您可以在测试类中使用这些方法来处理常见方案。如果您担心以后的更改会破坏所有测试,请将与您有关的部分放在可以根据需要更改的特定方法中。

关于测试太长/很难知道其中有什么:不要测试与其相关的每个断言的单个场景。分解它,测试它应该在y发生时记录x消息(1测试),它应该在y发生时保存到db(另一个单独的测试),它应该在z发生时向第三方发送请求(还有另一个测试)等。

进行集成/系统测试而不是这些单元测试:您可以从当前情况看出有很多场景&系统中那部分涉及的变化很小。这就是用那些模拟替换更多逻辑的盾牌。模拟不同条件的容易程度。对整个事情做同样的事情会为你的场景增加一个全新的复杂程度,如果你想要覆盖一系列场景,这肯定是无法管理的。

imho你应该尽量减少你为系统测试留下的组合,运行一些主要的场景应该已经告诉你很多系统工作正常 - 它应该是关于所有被正确连接的东西。

如上所述,我建议为您所拥有的所有集成代码添加集中式集成测试,这些集成代码可能当前未被测试覆盖/因为根据定义,单元测试无法实现。这将特别针对集成代码以及您期望的所有变体进行练习 - 相应的测试比在整个系统环境中尝试达到这些行为要简单得多。如果这些部分中的任何假设引起麻烦,请尽快告诉你。

答案 5 :(得分:1)

如果您认为单元测试太难了,请改为:将后置条件添加到Run方法中。后置条件是指对代码进行断言。例如,在该方法结束时,您可能希望某个变量保留特定值或某些可能选择中的一个值。

之后,您可以推导出该方法的前提条件。这基本上是每个参数的数据类型以及每个参数的限制约束(以及在方法开头初始化的任何其他变量)。

通过这种方式,您可以确定输入和输出都是您想要的。

这可能仍然不够,所以你必须逐行查看方法的代码,并寻找你想要做出断言的大部分。如果你有一个If语句,你应该在它之前和之后检查一些条件。

如果您知道如何检查对象的参数是否有效以及您知道所需的输出范围,则不需要任何模拟对象。

答案 6 :(得分:0)

你的测试太复杂了。

您应该测试类的方面,而不是为每个类的成员编写单元测试。单元测试不应涵盖成员的整个功能。

答案 7 :(得分:0)

我猜测Run()的每个测试都会对他们调用mocks的每个方法设置期望,即使该测试不专注于检查每个这样的方法调用。我强烈建议你谷歌“嘲笑不是存根”,并阅读福勒的文章。

此外,50行代码非常复杂。通过该方法有多少代码路径? 20+?您可能会受益于更高级别的抽象。我需要看一些代码来更加肯定地判断。