【问题标题】:Should I test (duplicate) data, or only the behavior?我应该测试(重复)数据,还是只测试行为?
【发布时间】:2018-04-25 21:48:32
【问题描述】:

从设计的角度来看,我想知道我是否应该测试数据,特别是如果它是一个众所周知的数据(不是非常可配置的) - 这可以适用于流行的文件扩展名、特殊 IP 之类的东西地址等

假设我们有一个紧急电话号码分类器:

public class ContactClassifier {

    public final static String EMERGENCY_PHONE_NUMBER = "911";

    public boolean isEmergencyNumber(String number) {
        return number.equals(EMERGENCY_PHONE_NUMBER);
    }
}

我应该这样测试吗(“911”重复):

@Test
public testClassifier() {
    assertTrue(contactClassifier.isEmergencyNumber("911"));
    assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
}

或(测试是否正确识别“配置”数字):

@Test
public testClassifier() {      
    assertTrue(contactClassifier.isEmergencyNumber(ContactClassifier.EMERGENCY_PHONE_NUMBER));
    assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
}

或在构造函数中注入“911”,这对我来说看起来最合理,但即使我这样做 - 如果组件被实例化,我是否应该为“应用程序胶水”编写测试具有适当的价值?如果有人可以在数据(代码)中打错字,那么我看不出有人可以在测试用例中打错字(我敢打赌这样的数据会是复制粘贴)

【问题讨论】:

    标签: java unit-testing junit automated-tests


    【解决方案1】:

    您可以测试的测试数据有什么意义?那常数值其实就是常数值?它已经在代码中定义了。 Java 确保该值实际上就是该值,所以不要打扰。

    在单元测试中你应该做的是测试实现,不管它是否正确。要测试不正确的行为,您使用在测试中定义的数据,标记为错误,然后发送到方法。要测试数据是否正确,请在测试期间输入它,如果它是未知的边界值,或者如果它们已经在某处定义,则使用应用程序范围内的已知值(接口内的常量)。

    困扰你的是数据,应该是众所周知的)被放置在测试中,这根本不正确。您可以做的是将其移至界面级别。这样,通过设计,您的应用程序已知数据设计为合同的一部分,并由 java 编译器检查其正确性。

    不应检查众所周知的值,而应由某种接口处理以维护它们。更改它很容易,是的,并且您的测试在更改期间不会失败,但为避免发生意外,您应该有合并请求、评论和与之相关的任务。如果有人不小心更改了它,您可以在代码审查中找到它。如果你把所有事情都提交给掌握,那么你会遇到比双重定义的常量更大的问题。

    现在,谈谈在其他方法中困扰您的部分:

    1)如果有人可以在数据(代码)中打错字,那么我认为没有理由有人可以在测试用例中打错字(我敢打赌这样的数据会是复制粘贴)

    实际上,如果有人更改数据中的值然后继续开发,在某个时候他会运行全新安装并查看那些失败的测试。那时他可能会更改/忽略测试以使其通过。如果您有人随机更改数据,那么您会遇到更大的问题,如果没有,并且更改是由任务定义的-您让某人进行了两次更改(至少?)。没有优点也有很多缺点。

    2) 担心某人犯错通常是不好的做法。您无法使用代码捕获它。代码审查就是为此而设计的。您可以担心有人没有正确使用您定义的界面。

    3) 我应该这样测试吗:

    @Test
    public testClassifier() {
        assertTrue(contactClassifier.isEmergencyNumber(ContactClassifier.EMERGENCY_PHONE_NUMBER));
        assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
    }
    

    也不是这样。这不是测试,而是测试批次,即同一方法中的多个测试。应该是这样的(convention-s):

    @Test
    public testClassifier_emergencyNumberSupplied_correctnessConfirmed() {
        assertTrue(contactClassifier.isEmergencyNumber(ContactClassifier.EMERGENCY_PHONE_NUMBER));
    }
    
    
    @Test
    public testClassifier_incorrectValueSupplied_correctnessNotConfirmed() {
        assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
    }
    

    4) 正确命名方法时没有必要,但如果它足够长,您可以考虑在 test.xml 中命名值。例如

    @Test
    public testClassifier_incorrectValueSupplied_correctnessNotConfirmed() {
        String nonEmergencyNumber = "111-other-222";
        assertFalse(contactClassifier.isEmergencyNumber(nonEmergencyNumber));
    }
    

    【讨论】:

    • 我刚刚意识到,但这不适用于可变值,例如 String[] 即使 final static 实际上仍然是可变的,并且您不应该公开可变值。我想将其添加为注释,但答案已被接受。
    【解决方案2】:

    这样的外部常量有问题。 导入消失,常量被添加到类的常量池中。因此,当将来更改原始类中的常量时,编译器不会看到 .class 文件之间的依赖关系,并将旧的常量值留在测试类中。

    所以你需要一个干净的构建。

    此外,测试应该简短、易读、快写。测试处理具体的数据案例。抽象会适得其反,甚至可能导致测试本身出现错误。 常量(如限速)应该刻在石头上,应该是文字。 价值属性比如汽车品牌的最大速度可以源于某种表格查找。

    当然,重复值可以放在 local 常量中。防止拼写错误,易于 - 作为本地 - 抽象,阐明值的语义。

    但是,一般情况下,可能会使用常量两次或三次(正面和负面测试),我会选择 裸常量

    【讨论】:

    • .class 文件非常重要!反对使用常量的有力论据。但是,我们仍然有能力通过静态方法暴露常量,但感觉代码不太清晰
    • “所以你需要一个干净的构建。”这实际上是一件好事,而不是一件坏事。一旦导入消失,.class 没有被删除的问题似乎更像是一个 ide 问题。
    【解决方案3】:

    在我看来,测试应该检查行为而不是内部实现。 isEmergencyNumber 验证您尝试测试的类中声明的常量的数字这一事实是对内部实现的验证。你不应该在测试中依赖它,因为它不安全。

    让我举几个例子:

    示例#1:有人错误地更改了EMERGENCY_PHONE_NUMBER,但没有注意到。第二个测试永远不会捕捉到它。

    示例#2:假设ContactClassifier 被不太聪明的开发人员更改为以下代码。当然,这完全是边缘情况,很可能在实践中永远不会发生,但它也有助于理解我的意思。

    public final static String EMERGENCY_PHONE_NUMBER = new String("911");
    
    public boolean isEmergencyNumber(String number) {
        return number == EMERGENCY_PHONE_NUMBER;
    }
    

    在这种情况下,您的第二个测试不会失败,因为它依赖于内部实现,但您的第一个检查真实单词行为的测试会发现问题。

    【讨论】:

    • 例如#1我认为更有可能故意更改类中的数据(“config”之类的),错误更改的可能性可能要小得多。另外,使用第一种方法,在更改数据后我们需要更新测试,因此我们可以说当我们触摸它们时,它们在更改期间并没有保护我们(在更改之前和之后运行相同的测试)。例如 #2 这不是 quaranteed,因为“String deduplicator”会导致测试仍然通过。
    • 我也觉得常量值“911”实现的一部分,而不是行为。数字“是 911”是一个实现细节,行为是“它应该为紧急号码返回 true”而不是“它应该为 911 返回 true”,因为它不是 is911Number() 方法而是 isEMERGENCYNumber()跨度>
    • 这些是有争议的陈述。根据我的经验,错误地改变很少发生,但它仍然会发生。我不认为“911”是实现细节。这是来自业务方面的数据,有些国家使用 999、112、150 等。第二个示例只是表明该方法可以被破坏并且只有通过常量才能通过测试,这意味着测试没有'做它的工作。
    【解决方案4】:

    编写单元测试有一个重要目的:指定被测试方法要遵循的规则。 因此,当该方法违反该规则,即行为发生变化时,测试将失败。 我建议,用人类语言写下你想要的规则,然后用计算机语言相应地写出来。 让我详细说明。

    选项 1 当我问ContactClassifier.isEmergencyNumber 方法时,“字符串“911”是紧急电话号码吗?”,它应该说是。 翻译成

    assertTrue(contactClassifier.isEmergencyNumber("911")); 
    

    这意味着您要控制和测试常量ContactClassifier.EMERGENCY_PHONE_NUMBER 指定的数字。它的值应该是 911,并且 isEmergencyNumber(String number) 方法会针对这个“911”字符串执行其逻辑。

    选项2当我问ContactClassifier.isEmergencyNumber方法时,ContactClassifier.EMERGENCY_PHONE_NUMBER中指定的字符串是紧急号码吗?”,它应该说是。

    翻译成

    assertTrue(contactClassifier.isEmergencyNumber("911")); 
    

    这意味着你不关心常量ContactClassifier.EMERGENCY_PHONE_NUMBER 指定了什么字符串。只是 isEmergencyNumber(String number) 方法针对该字符串执行其逻辑。

    因此,答案将取决于您要确保上述行为中的哪一种。

    【讨论】:

      【解决方案5】:

      我会选择

      @Test
      public testClassifier() {
          assertTrue(contactClassifier.isEmergencyNumber("911"));
          assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
      }
      

      因为这不会针对被测类中可能有错误的东西进行测试。测试

      @Test
      public testClassifier() {      
          assertTrue(contactClassifier.isEmergencyNumber(ContactClassifier.EMERGENCY_PHONE_NUMBER));
          assertFalse(contactClassifier.isEmergencyNumber("111-other-222"));
      }
      

      如果有人在ContactClassifier.EMERGENCY_PHONE_NUMBER 中输入错字,将永远无法捕捉到。

      【讨论】:

      • 更进一步,当它被声明为类常量并公开可用时,您是否会选择测试所有常量/枚举值,例如assertEquals("911" , Clssifier.EMERGENCY_NUM )?也许应该是选项 2 + 一个单独的“定义值”测试
      • 一个单独的“定义值”测试:理论上,这可能对公开可见的常量有用,但实际上这样的测试很可能是通过复制粘贴来自被测类的值来编写的,变得只是浪费时间,只是捕捉后来引入的错别字。当然也有使用常量的代码,如果这些方法得到错误的常量,其测试可能会失败,就像你的 isEmergencyNumber() 方法一样。
      • 当然有使用常量的代码,其测试可能会失败 - 我不能完全同意其他一些测试应该“测试我的常量”,如果是进一步的外部类从我的责任来看,它应该更多地依赖于我的班级已经过测试,并且不需要更多地测试它。我希望他们假设常数是好的并依赖于测试中的参考(而不是值)。如果没有一些信任,一切都需要测试驱动大量测试代码的所有内容
      【解决方案6】:

      在我看来,没有必要测试这个逻辑。原因是:这个逻辑对我来说是微不足道的。 我们可以测试所有代码行,但我认为这样做不是一个好主意。例如 getter 和 setter。如果我们按照理论来测试所有的代码行,我们必须为每个 getter 和 setter 编写测试。但是这些测试的价值很低,并且需要花费更多的时间来编写和维护。这不是一个好的投资

      【讨论】:

        猜你喜欢
        • 2010-10-25
        • 1970-01-01
        • 2011-12-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-29
        • 2018-02-21
        • 2013-10-23
        相关资源
        最近更新 更多