【问题标题】:TDD and encapsulation priority conflictTDD和封装优先级冲突
【发布时间】:2011-02-06 17:34:18
【问题描述】:

我刚开始在我的项目中练习 TDD。我现在正在开发一个项目,使用 php/zend/mysql 和 phpunit/dbunit 进行测试。我对封装和测试驱动方法的想法有点分心。我在封装背后的想法是隐藏对多个对象功能的访问。更清楚地说,私有和受保护的函数不能直接测试(除非您将创建一个公共函数来调用它)。

所以我最终将一些私有和受保护的函数转换为公共函数,以便能够测试它们。我真的违反了封装原则,让位于微功能可测试性。这是正确的做法吗?

【问题讨论】:

    标签: php unit-testing tdd phpunit encapsulation


    【解决方案1】:

    在 TDD 圈子中有一个非常标准的答案。如果您希望隐藏并直接测试某个类中的某个功能,您应该 sprout a class 使用该功能。这是 TDD 如何改进您的设计的一个很好的例子。

    在原始类中,多余的功能已经消失,封装在新类中,因此原始类的设计更简单,更符合Single Responsibility Principle。在发芽的类中,提取的功能是它的存在理由,因此它适合公开,因此无需仅测试修改即可测试。

    【讨论】:

    • 正是我需要的。新芽类对我来说是新的(也是其他 xunit 模式)。非常感谢
    • 这方面的好书是:有效地使用遗留代码。
    【解决方案2】:

    关于 Carl Manaster 的精彩回答,在开始 Carl 建议的道路之前,您至少应该考虑一些缺点。

    其中最重要的一点是:我们使用封装来最小化具有最大更改传播概率的潜在依赖项的数量。在您的情况下,您在类中封装了私有方法:它们对其他类不可用,因此对它们没有潜在的依赖关系:您对它们所做的任何更改的成本都被最小化并且传播到其他类的概率很低类。

    Carl 似乎建议将一些私有方法从您的类移到一个新类中,并将这些方法公开(以便您可以测试它们)。 (顺便说一句,为什么不在原来的课堂上公开它们呢?)

    通过这样做,您消除了其他类对这些方法形成依赖关系的障碍,如果任何其他类使用这些方法,这可能会增加更改这些方法的成本。

    您可能会认为这种不利的次要因素是值得为能够测试您的私有方法而付出的代价,但至少要意识到这一点。在少数情况下,这可能确实是值得的,但如果您在整个代码库中实施此操作,那么您将大大增加这些依赖关系形成的可能性,从而将您的维护周期成本增加到未知的程度。

    出于这些原因,我不同意 Carl 的建议,即“...... TDD 如何改进您的设计的一个很好的例子。”

    此外,他表示,“在原始类中,多余的功能已经消失,包装在新类中,因此原始类的设计更简单,更符合单一职责原则。”

    我认为被移动的功能根本不是“无关的”。此外,“更简单”是一个没有明确定义的情况:一个类的简单性与其大小成反比,但这并不意味着最简单的类系统将是最简单的系统:如果在这种情况下,所有的类将只包含一个方法,而一个系统将有大量的类;可以说,删除这个类内多方法的分层层会使系统变得更加复杂。

    此外,单一职责原则 (SRP) 是出了名的主观,完全取决于观察者的抽象级别。从类中删除方法并不会自动提高其对 SRP 的一致性。具有 10 个方法的 Printer 类只负责在类的抽象级别进行打印。它的一种方法可能是 checkPrinterConnected(),一种可能是 checkPaper();在方法级别,这些显然是独立的职责,但它们不会自动建议应将类分解为更多类。

    Carl 总结道,“在萌芽类中,提取的功能是它存在的理由,因此它适合公开,因此无需仅测试修改即可测试。”功能的重要性(它的存在理由)并不是其是否公开的基础。功能公开的适当性的基础是最小化暴露给客户端的接口,以便在最大化客户端对功能实现的独立性的同时,可以使用类的功能。当然,如果您只是将一种方法移动到发芽类中,那么它必须是公共的。但是,如果您要移动多个方法,则必须将那些对客户成功使用该类至关重要的方法公开:这些公共方法可能远不如您希望屏蔽的一些私有方法重要客户。 (无论如何,我不喜欢“存在理由”这个短语,因为方法的重要性也没有明确定义。)

    Carl 建议的另一种方法取决于您设想系统增长到多大。如果它会增长到少于几千个类,那么您可能会考虑使用一个脚本将您的源代码复制到一个新目录,将复制的源代码中所有出现的“私有”更改为“公共”,然后编写您的针对复制的源进行测试。这样做的缺点是复制代码需要时间,但好处是保留了原始源的封装,同时使所有方法都可以在复制的版本中进行测试。

    以下是我为此目的使用的脚本。

    问候,

    艾德·柯万

    !/bin/bash

    rm -rf 代码复制

    echo 创建代码副本 ...

    mkdir 代码复制

    cp -r ../www 代码复制/

    对于我在find code-copy -name "*php" -follow;做

    sed -i 's/private/public/g' $i
    

    完成

    php run_tests.php

    【讨论】:

    • 谢谢.. 这是另一个值得考虑的好事情。我将等待更多的见解,并正在对此主题进行自己的研究。
    • 这不会让测试变慢吗?
    【解决方案3】:

    我刚刚阅读了一篇关于让模拟对象驱动您的设计的精彩文章:

    http://www.mockobjects.com/files/usingmocksandtests.pdf

    当 Carl 说“您应该创建一个具有该功能的类”时,本文的作者解释了您的测试如何指导您,通过使用模拟对象,您如何设计您的类,以便 1) 不要需要担心无法测试私密部分,更重要的是 2)这将如何通过(我将引用 Carls 的话)发现具有正确责任的合作者和角色来改进您的设计。

    作者通过一个例子一步一步的把他的观点说得很清楚。

    这是另一篇采用相同方法的文章:

    http://www.methodsandtools.com/archive/archive.php?id=90

    引用:

    许多从 TDD 开始的人都在苦苦挣扎 掌握依赖关系。到 测试一个对象,你锻炼一些 行为,然后验证是否 对象处于预期状态。 因为OO设计专注于 行为,对象的状态是 通常隐藏(封装)。成为 能够验证对象是否行为 像预期的那样,你有时需要 访问内部状态和 引入特殊方法暴露 这种状态,如 getter 方法或 检索内部的属性 状态。

    除了不想要的东西 混乱他们的界面和 暴露他们的私处,我们 既不想引入不必要的 具有此类额外吸气剂的依赖项。 我们的测试会变得太紧 耦合并专注于实施 详情。

    一组敏捷软件开发 来自英国的先驱者是 也在为此苦苦挣扎 1999. 他们不得不添加额外的 getter 方法来验证状态 的对象。他们的经理不喜欢 所有这些对封装的破坏和 声明:我不想在 代码! (麦金农等人,2000 & 弗里曼等人,2004)

    团队想出了一个想法 专注于互动而不是 状态。他们创造了一个特殊的对象 更换合作者 被测对象。这些特别 对象包含规范 预期的方法调用。他们打电话给 这些对象模拟对象,或模拟 简而言之。最初的想法有 被细化,产生了几个 所有常见的模拟对象框架 编程语言:Java(jMock, EasyMock, Mockito), .NET (NMock, RhinoMocks), Python (PythonMock, Mock.py、Ruby(摩卡、RSpec)、C++ (模拟pp,amop)。看 www.mockobjects.com 了解更多 信息和链接。

    【讨论】:

    • 伟大的报价。这就是伦敦学校或外向内或模拟风格 TDD 开始与基于状态验证的经典 TDD 进行比较的方式。对我来说,mockist 风格的缺点是它有时有脆弱的测试,而经典风格的缺点是它打破了封装以便能够验证测试。
    猜你喜欢
    • 2014-07-23
    • 2010-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-26
    • 2013-07-12
    • 1970-01-01
    相关资源
    最近更新 更多