代码之家  ›  专栏  ›  技术社区  ›  Yishai

如何使用Mocks避免重复逻辑

  •  7
  • Yishai  · 技术社区  · 17 年前

    我遇到的问题是,我注意到一种模式,其中逻辑在概念上在多个地方重复。例如,我需要检测数据库中的值不存在,因此在这种情况下,我可能会从方法中返回null。因此,我有一个数据库访问类,它执行数据库交互,并适当地返回null。然后,我有一个业务逻辑类,它从模拟中接收null,然后在值为null时进行测试以采取适当的行动。

    更新:

    让我试着澄清我的问题。我在想,当代码随着时间的推移而发展时,如何确保通过模拟测试的类和模拟所代表的类的实际实现之间的集成不会中断。

    例如,我刚刚有一个案例,我有一个最初创建的方法,不期望为空值,所以这不是对真实对象的测试。然后,类的用户(通过模拟测试)被增强,在某些情况下可以将null作为参数传递。集成中断,因为真正的类没有进行null测试。现在,在最初构建这些类时,这并不是什么大问题,因为您在构建过程中测试了两端,但如果设计需要在两个月后发展,而您往往忘记了细节,您将如何测试这两组对象之间的交互(通过模拟测试的对象与实际实现)?

    潜在的问题似乎是重复(这违反了DRY原则),期望实际上被保留在两个地方,尽管这种关系是概念性的,但没有实际的重复代码。

    阅读与Aaron的交互就触及了问题的核心,但我真正想要了解的是如何避免或管理明显的重复,这样真实类行为的变化就会在与模拟交互的单元测试中显示为损坏的东西。显然,这不会自动发生,但可能有一种方法可以正确设计场景。

    11 回复  |  直到 17 年前
        1
  •  5
  •   womp    17 年前

    您所描述的是添加一个必须测试的全新状态,而不是空结果,现在数据库中出现了一些对象。你的测试套件怎么可能知道被测对象对一些新的随机对象的预期行为是什么?你需要编写一个新的测试。

    正如你所说,这种模仿不是“行为不端”。模拟正在执行您设置的操作。规范更改的事实对模拟没有影响。在这种情况下,唯一的问题是实施更改的人忘记更新单元测试。实际上,我不太确定为什么你认为存在任何重复的担忧。

    向系统添加一些新返回结果的编码器负责添加一个单元测试来处理这种情况。如果该代码也100%确定 不可能

    mocking的全部目的是将测试与实际资源解耦。它不会自动让你避免在系统中引入错误。如果你的单元测试在收到null时准确地描述了行为,那太好了!但是这个测试不应该知道任何其他状态,当然也不应该以某种方式被告知外部资源将不再发送空值。

        2
  •  4
  •   Don McCaughey    17 年前

    你没有错过什么。这是使用模拟对象进行单元测试的一个弱点。听起来你正在正确地将单元测试分解为合理大小的单元。这是一件好事;更常见的情况是,人们在“单元”测试中测试得太多。

    所以总结一下:

    1. 编写一些集成/功能测试来捕捉协作对象的问题
        3
  •  2
  •   Community Mohan Dere    6 年前
        4
  •  2
  •   Nat    17 年前

    [EDIT]基于注释:模拟只能允许实例化被测类并允许收集其他信息。尤其是,它必须 从不 影响你想要测试的结果。

    [EDIT2]模拟数据库意味着您不关心DB驱动程序是否工作。你想知道的是你的代码是否能正确解释数据库返回的数据。此外,这是测试错误处理是否正常工作的唯一方法,因为您无法告诉真正的DB驱动程序“当您看到此SQL时,抛出此错误”。这只能在模拟中实现。

    我同意,这需要一些时间来适应。以下是我的做法:

    • 我有测试来检查SQL是否有效。每个SQL都会对静态测试数据库执行一次,我会验证返回的数据是否符合我的期望。

        5
  •  1
  •   MrFox    17 年前

    问题


    但是,在某些场景中,你的测试不会失败——尽管它应该:

    当你编写代码时,你会多次使用你的方法。你会得到方法定义和使用之间的1:n关系。使用该方法的每个类都将在相应的测试中使用它的模拟。因此,该模拟也被使用了n次。

    你的方法结果曾经被认为永远不会 null 。更改此设置后,您可能会记得修复相应的测试。到现在为止,一直都还不错。

    .

    随着时间的推移,你忘了什么。..模拟永远不会返回 空值 空值 .

    你的 QA将失败

    解决方案

    为了避免这样的问题,你必须从一开始就编写更好的测试。如果你错过了测试类应该处理错误或 空值 这就像没有测试类的所有函数。

    稍后很难添加此内容。-所以早点开始,做大量的测试。

    缺少的相应测试不会出现在代码覆盖率中。

    这不仅包括 传递错误参数 (比如 空值 值)。 你的嘲笑也是外界的一部分。 通过 空值

    空值 为有效值-这些测试稍后将失败(因为缺少异常)。

    因为每个调用类都处理错误或 空值 不同-可以避免的不是重复代码。不同的治疗需要不同的测试。


    提示: 保持你的模型简单干净。将预期的返回值移动到测试方法。(你的模拟可以简单地将它们传递回去。)避免在模拟中测试决策。

        6
  •  1
  •   Justin Standard    17 年前

    您正在使用实体的模拟对象,使用JMock测试应用程序的业务层。您还可以使用DBUnit测试DAO层(应用程序和数据库之间的接口),并传递填充有一组已知值的实体对象的真实副本。因为您使用了两种不同的方法来准备测试对象,所以您的代码违反了DRY,并且随着代码的更改,您的测试可能会与现实不同步。

    福尔沃说。..

    Mocks Aren't Stubs 嘲笑者 方式,而“实物”路线是 执行测试的方法。

    在解决这个问题时,尽可能保持干燥的一种方法是 嘲笑者

    用户制造商避免重复

    制造商

    总是

        7
  •  1
  •   Tormod    17 年前

    单元测试不应该关心实现细节。

    如果您计划稍后将此条件细化,那么您应该将NULL检查包装成返回信息异常、枚举或显式命名的bool的内容。

    此外,您需要在单元测试代码中保持与生产代码相同的纪律,避免出现重复和功能嫉妒的味道。

        8
  •  0
  •   eglasius    17 年前

    如果不考虑上述内容,在初始场景中仍然有两个重要因素在起作用。您希望对单元测试代码给予与其他代码相同的关注,这意味着保持它们干燥是合理的。如果你在做TDD,这甚至会在一开始就把这个问题推到你的设计中。如果你对此不感兴趣,另一个相反的因素是YAGNI,你不想在代码中得到每一个(不)可能的场景。所以,对我来说,这将是:如果我的测试告诉我我遗漏了什么,我会仔细检查测试是否正常,然后继续进行更改。我确保在测试中不要做假设情景,因为这是一个陷阱。

        9
  •  0
  •   Asa Ayers    16 年前

    如果我正确理解了这个问题,那么您有一个使用模型的业务对象。有一个测试测试BO和模型之间的交互(测试a),还有一个测试是测试模型和数据库之间的交互的(测试B)。测试B更改为返回一个对象,但该更改不会影响测试A,因为测试A的模型被模拟了。

        10
  •  -1
  •   Rune Sundling    17 年前

    但是,我可以通过快速阅读来提取的意思对我来说没有什么意义,因为你想要一个不改变合同的变化来影响模拟的工作方式。

    模拟使您能够专注于测试系统的特定部分。模拟部分将始终以指定的方式工作,测试可以专注于测试它应该测试的特定逻辑。因此,您不会受到无关逻辑、延迟问题、意外数据等的影响。

    您可能会有单独数量的测试在另一个上下文中检查模拟功能。

        11
  •  -2
  •   Serhat Ozgel    17 年前

    我认为你的问题违反了利斯科夫替代原则:

    子类型必须可替换其基类型

    从技术上讲,你可能看起来依赖于一个抽象,但在你所说的场景中,你并没有真正依赖于抽象,你实际上依赖于一种实现。你说“如果这个方法改变了它的行为,它的用户就会崩溃,我的测试永远不会知道”。在单元测试层面上,你是对的。但在合同层面上,以这种方式改变行为是错误的。因为通过更改方法,您显然违反了方法与其调用者之间的约定。

    你为什么改变一种方法?很明显,该方法的调用者现在需要不同的行为。所以,你要做的第一件事不是改变方法本身,而是改变你的客户端所依赖的抽象或契约。他们必须首先改变并开始使用新的契约:“好吧,我的需求发生了变化,我不再希望这个方法返回,在这个特定的场景中,这个接口的实现者必须返回这个”。因此,您需要更改界面,根据需要更改界面的用户,这包括更新他们的测试,最后一件事是更改传递给客户端的实际实现。这样,您就不会遇到您所说的错误。

    所以,

    class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
    ...
    AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
    
    1. 测试需求工作,并确保它与新行为一起工作。

    这看起来很可怕,但在现实生活中,对于小的变化来说,这是微不足道的,对于大的变化来说是痛苦的,事实上,这是必须的。