【问题标题】:Unit Testing : what to test / what not to test?单元测试:测试什么/不测试什么?
【发布时间】:2012-01-24 11:12:13
【问题描述】:

从几天前开始,我开始对 C# 和 VS2010 中的单元测试和 TDD 感兴趣。我已经阅读了博客文章、观看了 youtube 教程以及更多解释为什么 TDD 和单元测试对您的代码如此有益以及如何进行的内容。

但我发现最大的问题是,我不知道在我的测试中检查什么以及不检查什么。

我知道我应该检查所有逻辑操作、引用和依赖关系的问题,但是例如,我应该为应该是用户输入的字符串格式创建一个单元测试吗?还是只是浪费我的时间,而我只能在实际代码中检查它?

有什么指南可以澄清这个问题吗?

【问题讨论】:

    标签: unit-testing tdd


    【解决方案1】:

    在 TDD 中,每一行代码都必须通过在代码之前编写的失败测试用例来证明。

    这意味着您不能在没有测试用例的情况下开发任何代码。如果你有一行代码(条件、分支、赋值、表达式、常量等)可以修改或删除而不会导致任何测试失败,则说明这行代码没用,应该 em> 已删除(或者您缺少支持其存在的测试)。

    这有点极端,但这就是 TDD 的工作原理。话虽如此,如果您有一段代码并且想知道是否应该对其进行测试,那么您没有正确执行 TDD。但是,如果您有一个字符串格式化例程或变量递增或任何一小段代码,那么必须有一个支持它的测试用例。

    更新(Ed. 建议的用例):

    例如,将一个对象添加到列表中并创建一个测试以查看它是否真的在里面,或者当列表不应该允许它们时是否存在重复。

    这是一个反例,您会惊讶于发现复制粘贴错误的难度以及它们的普遍性:

    private Set<String> inclusions = new HashSet<String>();
    private Set<String> exclusions = new HashSet<String>();
    
    public void include(String item) {
        inclusions.add(item);
    }
    
    public void exclude(String item) {
        inclusions.add(item);
    }
    

    另一方面,单独测试 include() 和 exclude() 方法是一种过度杀伤力,因为它们本身并不代表任何用例。但是,它们可能是某些业务用例的一部分,您应该进行测试。

    显然你不应该在分配后测试x = 7 中的x 是否真的是7。测试生成的 getter/setter 也是一种矫枉过正的做法。但它是最容易出错的代码。由于复制和粘贴错误或拼写错误(尤其是在动态语言中),这经常发生。

    另见:

    【讨论】:

    • +1,但这真的很极端,对我来说通常不值得,请参阅我的回答。另外,如果你有一堆实现性能优化的行,这是否意味着你有 unit 测试来衡量性能?否则,您的逻辑将认为这些行是多余的。
    • 在我的示例中,我正在考虑最终用户应该在文本框中写入的字符串。我想这必须检查,因为格式是至关重要的,但有时我觉得我正在考虑测试语言,而不是逻辑。例如,将一个对象添加到列表中并创建一个测试以查看它是否真的在里面,或者当列表不允许它们时存在重复。
    • 您测试某物的愿望应该与该物损坏的概率及其对最终产品的重要性成正比。如果没关系,请不要测试它。如果它不能破坏(就像编译器合成的访问器),不要测试它。如果它是产品的关键部分,请尽一切可能对其进行测试。
    • 这个不多说了。你从超严格但清晰的定义开始:测试每一行,包括条件、分支、赋值、表达式、常量等。然后你说,不要测试所有东西,举两个例子。那么应该和不应该测试什么?你是说这取决于程序员自己的判断吗?
    • 你不测试行,你测试功能。您编写了足够多的测试,以使每个函数中的每一行/分支至少执行一次。函数的输出必须是正确的,如果它是你不关心个别行。然后,只要测试仍然通过,您就可以放心地重构您的代码。您也绝对可以编写性能测试。您不能直接测试私有功能,只能测试应该充分行使私有功能的公共功能。如果您有未使用的私有函数,则应将其删除。
    【解决方案2】:

    您最初的几个 TDD 项目可能会导致 更糟糕的设计/重新设计,并且在您学习的过程中需要更长的时间才能完成(至少以我的经验而言)。这就是为什么您不应该在大型关键项目中使用 TDD。

    我的建议是在几个小项目(100-10,000 LOC)上使用“纯”TDD(验收/单元测试一切测试优先)。要么自己做副项目,要么如果你在空闲时间不编码,就在小型内部实用程序上使用 TDD 来完成你的工作。

    在您对大约 6 到 12 个项目进行“纯” TDD 之后,您将开始了解 TDD 如何影响设计并学习如何进行可测试性设计。一旦您知道如何针对可测试性进行设计,您将需要减少 TDD 并最大化单元、回归、验收等测试的 ROI,而不是预先测试所有内容。

    对我来说,TDD 更像是一种用于良好代码设计的教学方法,而不是一种实用的方法。不过,我还是用TDD逻辑代码和单元测试代替debug。

    【讨论】:

    • 昨天,当我创建一些小型原型时,我意识到了这一点。这确实是另一种在代码中思考的全新方式,所以在我的大脑习惯它之前,它可能需要很长时间。对于一个大项目来说,情况会更糟。非常感谢您的建议!
    【解决方案3】:

    这个问题没有简单的答案。行动中存在收益递减规律,因此实现完美覆盖很少值得。知道要测试什么是经验,而不是规则。最好在进行过程中有意识地评估过程。有什么东西坏了吗?测试可行吗?如果没有,是否可以重写代码以使其更可测试?将来总是对此类情况进行测试是否值得?

    如果您将代码拆分为模型、视图和控制器,您会发现大部分关键代码都在模型中,并且这些代码应该是可测试的。 (这是 MVC 的要点之一。)如果一段代码很关键,我会对其进行测试,即使这意味着我必须重写它以使其更具可测试性。如果一段代码很容易出错或被未来的更新破坏,它就会得到测试。我很少测试控制器和视图,因为这对我来说不值得。

    【讨论】:

    • 我正计划在下一个项目中使用 MVC 模式,所以即使您说的内容一旦考虑就很明显,这非常有帮助。
    • 我同意“知道要测试什么是经验,而不是规则”。但作为一个 TDD 新手,如果试图在更多测试方面犯错以获得这种经验,那么 OP 会得到很好的服务。
    【解决方案4】:

    在我看来,您的所有代码都属于以下三个类别之一:

    1. 易于测试的代码:这包括您自己的确定性公共方法。
    2. 难以测试的代码:这包括 GUI、非确定性方法、私有方法和具有复杂设置的方法。
    3. 您不想测试的代码:这包括第 3 方代码,以及难以测试且不值得努力的代码。

    在这三者中,您应该专注于测试简单代码。 难以测试的代码应该被重构为两部分:你不想测试的代码和简单的代码。当然,您应该测试重构后的简易代码。

    【讨论】:

    • 嗨,我想询问在运行时将注入依赖项的框架中的方法,如果我什至不知道输入是什么,我应该如何首先测试它们?例如,该方法期望 HttpRequestMessage 作为输入,我如何确定它会携带我想要的东西?如何精确地模拟它?
    【解决方案5】:

    Kent Beck 在 Extreme Programming Explained 中说,您只需要测试需要在生产中工作的东西。

    这是封装测试驱动开发的一种粗暴方式,其中生产代码中的每个更改都由一个测试支持,当更改不存在时该测试失败;和 You Ain't Gonna Need It,它表示为只处理几个特定情况的应用程序创建通用类没有任何价值。

    【讨论】:

    【解决方案6】:

    我认为您应该只对系统行为的入口点进行单元测试。这包括公共方法、公共访问器和公共字段,但不包括常量(常量字段、枚举、方法等)。它还包括任何直接处理 IO 的代码,我将在下面进一步解释原因。

    我的推理如下:

    1. 所有公开的内容基本上都是系统行为的入口点。因此,应该编写一个单元测试来保证该入口点的预期行为按要求工作。您不应该测试调用入口点的所有可能方式,只测试您明确需要的方式。因此,您的单元测试也是您的系统支持的行为规范以及如何使用它的文档。

    2. 不公开的东西基本上可以随意删除/重构,不会影响系统的行为。如果你要测试这些,你会创建一个从你的单元测试到那个代码的硬依赖,这会阻止你对其进行重构。这就是为什么除了公共方法、字段和访问器之外,您不应该测试其他任何东西。

    3. 设计的常量不是行为,而是公理。验证常量的单元测试本身就是一个常量,因此编写常量测试只会是重复的代码和无用的努力。

    所以回答你的具体例子:

    我是否应该为已被支持的字符串格式创建一个单元测试 成为用户输入?

    是的,当然。所有接收或发送外部输入/输出(可以概括为接收IO)的方法都应该进行单元测试。这可能是我说接收 IO 的非公开事物也应该进行单元测试的唯一情况。那是因为我认为 IO 是一个公共条目。任何我认为是公开的外部参与者的入口点。

    因此,对公共方法、公共字段、公共访问器进行单元测试,即使它们是静态构造,也要对从外部参与者(无论是用户、数据库、协议等)接收或发送数据的任何内容进行单元测试。

    注意:您可以在非公开事物上编写临时单元测试,以帮助确保您的实现正常工作。这更像是一种帮助您弄清楚如何正确实施它并确保您的实施按预期工作的方法。在你测试它可以工作之后,你应该删除单元测试或从你的测试套件中禁用它。

    【讨论】:

      【解决方案7】:

      我认为你必须改变你的观点。 在纯粹的形式中,TDD 需要红绿重构工作流程:

      • 写测试(它必须失败)RED
      • 编写代码以满足测试 GREEN
      • 重构您的代码

      所以问题是“我必须测试什么?”有这样的响应:“您必须编写一个与功能或特定要求相对应的测试”。

      通过这种方式,您可以获得必须的代码覆盖率和更好的代码设计(请记住,TDD 也代表测试驱动的“设计”)。

      一般来说,您必须测试所有公共方法/接口。

      【讨论】:

        【解决方案8】:

        我是否应该为已被允许的字符串格式创建一个单元测试 成为用户输入?还是只是在浪费我的时间而我可以检查 它在实际代码中?

        不确定我理解你的意思,但你在 TDD 中编写的测试应该是为了测试你的生产代码。它们不是检查用户输入的测试。

        换句话说,可以有 TDD 单元测试来测试用户输入验证代码,但不能有 TDD 单元测试来验证用户输入本身。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-11-21
          • 2018-01-18
          • 1970-01-01
          • 2010-09-11
          • 1970-01-01
          • 2010-10-05
          • 1970-01-01
          • 2011-12-31
          相关资源
          最近更新 更多