【问题标题】:Should unit tests be written for getter and setters?是否应该为 getter 和 setter 编写单元测试?
【发布时间】:2011-09-06 01:03:15
【问题描述】:

我们是否应该为我们的 getter 和 setter 编写测试,还是有点矫枉过正?

【问题讨论】:

  • 我不这么认为。您不应该为 getter/setter 编写测试用例。
  • 我假设你的意思是Java?这对于 Java 来说是一个特别尖锐的问题,对于更现代的语言来说更是如此。
  • @skaffman 哪些现代语言没有属性?当然,像 Java 这样的语言要求它们是完整的方法体,但这并没有使它与 C# 在逻辑上有所不同。
  • @Claus:他没有说属性,他说的是 getter 和 setter。在 java 中你手动编写这些,在其他语言中你会得到更好的支持。

标签: unit-testing


【解决方案1】:

我会说不。

@Will 说您应该以 100% 的代码覆盖率为目标,但在我看来,这是一种危险的分心。您可以编写具有 100% 覆盖率的单元测试,但绝对不进行任何测试。

单元测试以一种富有表现力和意义的方式测试代码的行为,而 getter/setter 只是达到目的的一种手段。如果您的测试使用 getter/setter 来实现测试“真实”功能的目标,那么这就足够了。

另一方面,如果您的 getter 和 setter 不仅仅只是 get 和 set(即它们是相当复杂的方法),那么是的,应该对它们进行测试。但是不要写一个单元测试用例只是来测试一个getter或setter,那是浪费时间。

【讨论】:

  • 追求 100% 的代码覆盖率是愚蠢的。您最终将覆盖未公开的代码和/或不复杂的代码。这些东西最好留给自动生成,但即使是自动生成的测试也毫无意义。最好将重点转移到重要的测试上,例如集成测试。
  • @Claus:我同意,除了关注集成测试。单元测试对于良好的设计至关重要,并补充集成测试。
  • 我认为在整个测试套件运行时 100% 的代码覆盖率是一个很好的目标。属性 getter setter 和其他代码作为较大测试的一部分运行。最后要介绍的部分可能是错误处理。如果错误处理没有被单元测试覆盖,它就永远不会被覆盖。您真的要发布包含从未运行过的代码的产品吗?
  • 我倾向于不同意这个建议。是的,getter 和 setter 很简单,但它们也很容易搞砸。几行简单的测试,您就知道它们正在工作并继续工作。
  • 您应该始终删除未使用的代码。请记住,对您的公共接口进行功能测试只是单元测试的一个好处。另一个好处是回归测试——即测试公共接口的完整性。您可能尚未拥有刚刚编写的代码,并且这些 getter 和 setter 很容易受到一些未来开发人员对其实现的影响。这基本上就是为什么开发人员可能认为它没用而她的老板认为它很重要的原因。
【解决方案2】:

Roy Osherove 在他的名著《单元测试的艺术》中说:

属性(Java 中的 getter/setter)是很好的代码示例,通常不包含任何逻辑,也不需要测试。但请注意:在属性中添加任何检查后,您需要确保正在测试逻辑。

【讨论】:

  • 考虑到测试这些所需的时间以及添加行为的可能性,我不明白为什么不这样做。我怀疑如果被按下,他可能会回答“好吧,我想为什么不”。
  • 重要的早期书籍往往有不好的建议。我见过很多这样的情况,人们快速地投入 getter 和 setter 并犯错误,因为他们正在剪切和粘贴或忘记这一点。或this->或类似的东西。它们很容易测试,当然应该进行测试。
【解决方案3】:

TDD 是肯定的


注意:尽管这可能是个糟糕的建议,但这个答案不断得到支持。要了解原因,请查看下面的its little sister


有争议,但我认为任何对这个问题回答“否”的人都缺少 TDD 的基本概念。

对我来说,如果您遵循 TDD,答案是响亮的是的。如果您不是,那么“否”是一个合理的答案。

TDD 中的 DDD

TDD 经常被引用为具有您的主要好处。

  • 防御
    • 确保代码可能会改变,但不会改变其行为
    • 这使得重构这一非常重要的实践成为可能。
    • 您是否获得此 TDD。
  • 设计
    • 指定某事应该做什么,它应该如何表现在实施之前
    • 这通常意味着更明智的实施决策
  • 文档
    • 测试套件应作为规范(要求)文档。
    • 为此目的使用测试意味着文档和实现始终处于一致状态 - 对一个的更改意味着对另一个的更改。与将要求和设计保存在单独的 word 文档中进行比较。

责任与实施分开

作为程序员,非常容易将属性视为重要的东西,而将 getter 和 setter 视为某种开销。

但属性是实现细节,而 setter 和 getter 是实际使程序工作的合同接口。

拼写一个对象应该:

允许其客户端更改其状态

允许其客户端查询其状态

然后如何实际存储此状态(其中一个属性是最常见的,但不是唯一的方式)。

一个测试例如

(The Painter class) should store the provided colour

对于 TDD 的 文档 部分很重要。

当您编写测试时,您应该不知道最终实现是微不足道的(属性)并且没有防御好处这一事实。

缺乏往返工程...

系统开发领域的一个关键问题是缺乏往返工程1 - 系统的开发过程被分割成不连贯的子过程其工件(文档、代码)通常不一致。

1Brodie, Michael L. “John Mylopoulos:缝合概念建模的种子”。概念建模:基础和应用。施普林格柏林海德堡,2009。1-9。

...以及 TDD 如何解决它

TDD 的文档部分确保了系统的规范及其代码始终保持一致。

先设计,后实施

在 TDD 中,我们首先编写失败的验收测试,然后才编写让它们通过的代码。

在更高级别的 BDD 中,我们先编写场景,然后让它们通过。

为什么要排除 setter 和 getter?

理论上,在 TDD 中完全有可能由一个人编写测试,而另一个人实现使其通过的代码。

所以问问自己:

为类编写测试的人是否应该提及 getter 和 setter。

由于getter和setter是类的公共接口,答案显然是,否则将无法设置或查询对象的状态。 然而,这样做的方法不一定是通过单独测试每个方法,请参阅我的other answer 了解更多信息。

显然,如果你先写代码,答案可能就不是那么清楚了。

【讨论】:

  • 这只是硬币的一方面。想想你可以做的事情,而不是仅仅为了测试而测试。正如 Kent Beck 所说,你是通过工作代码而不是工作测试获得报酬的。
  • @GeorgiiOleinikov 您在下面阅读了我的其他答案吗?很符合你的看法。
【解决方案4】:

tl;dr: 是的应该,而OpenPojo是微不足道的。

  1. 您应该在 getter 和 setter 中进行一些验证,因此您应该对其进行测试。例如,setMom(Person p) 不应允许将比自己年轻的任何人设置为他们的母亲。

  2. 即使您现在没有这样做,但将来您也很有可能会这样做,那么这对于回归分析来说将是一个很好的选择。如果您想允许将母亲设置为null,您应该对其进行测试,以便以后有人更改它,这将加强您的假设。

  3. 一个常见的错误是void setFoo( Object foo ){ foo = foo; },它应该是void setFoo( Object foo ){ this.foo = foo; }。 (在第一种情况下,正在写入的foo参数 不是 对象 上的foo 字段。

  4. 如果您要返回一个数组或集合,您应该在返回之前测试 getter 是否会执行传入 setter 的数据的defensive copies

  5. 否则,如果您有最基本的 setter/getter,那么对它们进行单元测试最多可能会增加每个对象大约 10 分钟,那么损失是多少?如果您添加行为,您已经有一个骨架测试,并且您可以免费获得此回归测试。如果您使用的是 Java,那么您没有任何借口,因为有 OpenPojo。您可以启用一组现有规则,然后使用它们扫描整个项目,以确保它们在您的代码中得到一致应用。

来自他们的examples

final Validator pojoValidator = ValidatorBuilder.create()
        .with(
            new NoPublicFieldsRule  (),
            new NoPrimitivesRule    (),
            new GetterMustExistRule (),
            new SetterMustExistRule ()
        )
        .with(
            new DefaultValuesNullTester (),
            new SetterTester            (),
            new GetterTester            ()
        )
        .build();


pojoValidator.validate(  PojoClassFactory.getPojoClasses( "net.initech.app", new FilterPackageInfo() )  );

【讨论】:

  • 我知道这已经过时了,但是:您应该在 getter 和 setter 中进行验证吗?我的印象是 setMom 应该按照它说的去做。如果它正在验证,那么它不应该是 validateAndSetMom 吗?或者更好的是,验证代码不应该存在于其他地方而不是一个简单的对象吗?我在这里错过了什么?
  • 是的,您应该始终验证您的输入。如果不是,那为什么不直接使用公共变量呢?它变成了同样的东西。使用 setter 与变量相比,它的全部好处是它允许您确保您的对象永远不会无效,例如年龄是一个负数。如果您不在对象中执行此操作,您将在其他地方执行此操作(例如“服务”god object),此时这并不是真正的 OOP。
  • 嗯,这可能是我这一周的亮点。谢谢。
【解决方案5】:

是的,但并不总是孤立的

请允许我详细说明:

什么是单元测试?

来自有效地使用遗留代码1

单元测试一词在软件开发中有着悠久的历史。普遍于 大多数单元测试的概念是认为它们是独立于个体的测试 软件的组成部分。什么是组件?定义不同, 但在单元测试中,我们通常关注系统中最原子的行为单元。在过程代码中,单元通常是函数。在面向对象的代码中,单位是类。

请注意,对于 OOP,您可以在其中找到 getter 和 setter,单位是 ,不一定是单个方法

什么是好的测试?

所有需求和测试都遵循Hoare logic的形式:

{P} C {Q}

地点:

  • {P} 是前提条件(给定
  • C是触发条件(when
  • {Q} 是后置条件 (then)

然后是格言:

测试行为,而不是实现

这意味着你不应该测试C如何实现后置条件,你应该检查{Q}C的结果。

对于 OOP,C 是一个类。所以你不应该测试内部效应,只测试外部效应。

为什么不单独测试 bean getter 和 setter

Getter 和 setter 可能涉及一些逻辑,但只要该逻辑没有外部影响 - 使它们成为 bean 访问器2)测试将不得不查看内部对象,因此不仅违反封装,而且还测试实现。

因此,您不应该孤立地测试 bean getter 和 setter。这很糟糕:

Describe 'LineItem class'

    Describe 'setVAT()'

        it 'should store the VAT rate'

            lineItem = new LineItem()
            lineItem.setVAT( 0.5 )
            expect( lineItem.vat ).toBe( 0.5 )

虽然如果setVAT 会抛出异常,但相应的测试将是合适的,因为现在外部影响。

应该如何测试 getter 和 setter?

如果这样的改变对外部没有影响,那么改变一个对象的内部状态实际上是没有意义的,即使这种影响稍后会出现。

所以对 setter 和 getter 的测试应该关注这些方法的外部影响,而不是内部影响。

例如:

Describe 'LineItem class'

    Describe 'getGross()'

        it 'should return the net time the VAT'

            lineItem = new LineItem()
            lineItem.setNet( 100 )
            lineItem.setVAT( 0.5 )
            expect( lineItem.getGross() ).toBe( 150 )

你可能会想:

等一下,我们在这里测试getGross() 不是 setVAT()

但如果setVAT() 发生故障,该测试应该同样失败。

1Feathers, M., 2004。有效地使用遗留代码。 Prentice Hall 专业人士。

2Martin, R.C.,2009 年。清洁代码:敏捷软件工艺手册。培生教育。

【讨论】:

    【解决方案6】:

    虽然属性有合理的理由,但面向对象设计的一个共同信念是,通过属性公开成员状态是糟糕的设计。 Robert Martin's article on the Open Closed Principle 对此进行了扩展,声明属性鼓励耦合,因此限制了关闭类以防止修改的能力——如果你修改了属性,类的所有使用者也需要更改。他认为暴露成员变量不一定是糟糕的设计,它可能只是糟糕的风格。但是,如果属性是只读的,那么滥用和副作用的可能性就会降低。

    我可以为单元测试提供的最佳方法(这可能看起来很奇怪)是使尽可能多的属性受保护或内部。这将防止耦合,同时阻止为 getter 和 setter 编写愚蠢的测试。

    应该使用读/写属性的原因很明显,例如绑定到输入字段的 ViewModel 属性等。

    更实际地,单元测试应该通过公共方法来驱动功能。如果您正在测试的代码碰巧使用了这些属性,您就可以免费获得代码覆盖率。如果事实证明这些属性从未被代码覆盖突出显示,则很有可能:

    1. 您缺少间接使用属性的测试
    2. 属性未使用

    如果您为 getter 和 setter 编写测试,您会得到一种错误的覆盖感,并且无法确定这些属性是否被函数行为实际使用。

    【讨论】:

      【解决方案7】:

      如果 getter 和/或 setter 的 cyclomatic complexity 是 1(它们通常是),那么答案是否定的,你不应该这样做。

      因此,除非您的 SLA 要求 100% 的代码覆盖率,否则不要费心,而是专注于测试软件的重要方面。

      附:记住要区分 getter 和 setter,即使在像 C# 这样的语言中,属性可能看起来是一样的。 setter 复杂度可能高于 getter,因此可以验证单元测试。

      【讨论】:

      • 虽然应该在 getter 上测试防御性复制
      【解决方案8】:

      幽默而明智的选择:The Way of Testivus

      “今天写出你能做的测试”

      如果您是一位经验丰富的测试人员并且这是一个小项目,那么测试 getter/setter 可能会有点过头了。但是,如果您刚刚开始学习如何进行单元测试,或者这些 getter/setter 可能包含逻辑(如 @ArtB 的 setMom() 示例),那么编写测试将是一个好主意。

      【讨论】:

      【解决方案9】:

      这实际上是我和我的团队之间最近的话题。我们争取 80% 的代码覆盖率。我的团队认为 getter 和 setter 是自动实现的,编译器在幕后生成一些基本代码。在这种情况下,鉴于生成的代码是非侵入性的,因此测试编译器为您创建的代码实际上没有任何意义。我们还讨论了异步方法,在这种情况下,编译器会在后台生成一大堆代码。这是一个不同的案例,我们会测试。长答案简短,与您的团队一起提出并自行决定是否值得测试。

      另外,如果您像我们一样使用代码覆盖率报告,您可以做的一件事是添加 [ExcludeFromCodeCoverage] 属性。我们的解决方案是将其用于仅具有使用 getter 和 setter 或属性本身的属性的模型。这样,在运行代码覆盖率报告时,它不会影响总代码覆盖率 %,假设这是您用来计算代码覆盖率百分比的方法。祝测试愉快!

      【讨论】:

        【解决方案10】:

        我做了一点analysis of the coverage achieved in the JUnit code itself

        一类未被发现的代码是“太简单而无法测试”。这包括简单的 getter 和 setter,JUnit 的开发人员对其进行测试。

        另一方面,JUnit 没有任何(非弃用)方法长度超过 3 行且未被任何测试覆盖。

        【讨论】:

          【解决方案11】:

          在我看来,代码覆盖率是查看是否遗漏了任何应覆盖的功能的好方法。

          当您通过漂亮的颜色手动检查覆盖率时,可以说不需要测试普通的 getter 和 setter(尽管我总是这样做)。

          如果您只检查项目的代码覆盖率,那么像 80% 这样的测试覆盖率是没有意义的。您可以测试所有不合逻辑的部分并忘记一些关键部分。在这种情况下,只有 100% 意味着您已经测试了所有重要代码(以及所有非逻辑代码)。只要达到 99.9%,您就知道忘记了什么。

          顺便说一句:代码覆盖率是最后检查你是否已经完全(单元)测试了一个类。但是 100% 的代码覆盖率并不一定意味着您已经实际测试了该类的所有功能。所以单元测试应该总是按照类的逻辑来实现。最后,您运行覆盖以查看是否忘记了任何内容。如果你做对了,你第一次就达到了 100%。

          还有一件事:最近在荷兰的一家大型银行工作时,我注意到 Sonar 显示 100% 的代码覆盖率。然而,我知道有些东西不见了。检查每个文件的代码覆盖率百分比表明文件的百分比较低。整个代码库百分比大到一个文件没有使百分比显示为 99.9%。所以你可能要注意这个...

          【讨论】:

            【解决方案12】:

            我会说:是的 getter/setter 方法中的错误会悄悄潜入并导致一些丑陋的错误。

            我编写了一个库来简化这个和其他一些测试。 你在 JUnit 测试中唯一需要写的是:

                    assertTrue(executor.execute(*TheClassIWantToTest.class*, Arrays.asList( new DefensiveCopyingCheck(),
                        new EmptyCollectionCheck(), new GetterIsSetterCheck(),
                        new HashcodeAndEqualsCheck(), new PublicVariableCheck())));
            

            ->https://github.com/Mixermachine/base-test

            【讨论】:

              【解决方案13】:

              是的,特别是如果要获取的项目是从抽象类继承的类的对象。您的 IDE 可能会或可能不会提醒您某个属性尚未初始化。

              然后一些明显不相关的测试因NullPointerException 而崩溃,您需要一段时间才能弄清楚实际上不存在可获取的属性。

              尽管这仍然没有在生产中发现问题那么糟糕。

              确保所有抽象类都有构造函数可能是个好主意。如果没有,getter 测试可能会提醒您注意那里的问题。

              至于基元的 getter 和 setter,问题可能是:我是在测试我的程序还是在测试 JVM 或 CLR?一般来说,JVM不需要测试。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-06-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多