【问题标题】:Deleting unit tests during TDD process在 TDD 过程中删除单元测试
【发布时间】:2014-10-17 21:05:00
【问题描述】:

我目前正在阅读一本关于使用 testNG 和 Mockito 进行测试的书,以提高我使用这些工具的技能、拓宽我对一般测试的了解并提高我创建的代码的质量。

在第 4 章“测试驱动开发”之后,我应该做很多练习来巩固我刚刚学到的 TDD 知识并养成循环红色测试 -> 实施代码 -> 绿色测试 -> 的习惯重构。其中之一称为“PasswordValidator”,它应该可以验证密码的强度。

推荐的此类开发方法之一是从最简单的测试开始,然后逐步编写更复杂的测试用例,实现更复杂的功能。说实话,通常我会通过编写一个适当的正则表达式来解决这个问题(例如这个:https://stackoverflow.com/a/5142164/2576122),编写一些测试来涵盖我可能想象的任何边界情况并完成。

在这里,我首先创建 PasswordValidatorTest 类并在其中进行测试,例如:

@Test
public void returnsTrueWhenPasswordIsAtLeastEightCharsLong() {
    assertTrue(atLeastEightCharactersLong(EIGHT_CHARACTERS_LONG_PASSWORD));
}

我检查它是否失败,在 PasswordValidator 中实现逻辑:

public static boolean atLeastEightCharactersLong(String passwordToValidate) {
    if (passwordToValidate.length() >= 8) {
        return true;
    } else {
        return false;
    }
}

我检查它是绿色的,然后我重构:

public static boolean atLeastEightCharactersLong(String passwordToValidate) {
    return (passwordToValidate.length() >= 8);
} 

包含数字、特殊符号等的测试也是如此。在这样的过程的某个时刻,我最终得到了一个最终测试:

@Test
public void shouldReturnTrueIfPasswordIsValid() {
    assertTrue(PasswordValidator.isValid(VALID_PASSWORD_EXAMPLE));
}

实现的目的:

public static boolean isValid(String passwordToValidate) {
    return atLeastEightCharactersLong(passwordToValidate) && containsAtLeastTwoDigits(passwordToValidate) &&
            containsAtLeastOneSpecialSign(passwordToValidate);
}

然后在测试为绿色之后,我可以清楚地看到,所有早期测试的方法从一开始就应该是私有的。

在编码 TDD 中最初有用的一些测试被删除的情况是否经常发生并且是否被接受?因为有一件事是肯定的——这些方法根本不应该暴露给那个类用户,所以像“让它们受到保护”这样的回答对我没有吸引力。

或者我的方法从一开始就无效,我应该只为 isValid() 方法编写一个测试来测试 API 的行为?如果我这样做了,那么这样的测试将过于笼统,并且不允许我在开发时检查所有边界情况,或者我可能全都错了?在要测试的内容和根本不应该测试的内容之间是否存在界限?粒度应该是多少?

【问题讨论】:

  • IMO,你的第一个测试应该测试验证器应该拥有的唯一公共方法(isValid())并且是isValidShouldReturnFalseIfPasswordIsLessThan8CharactersLong()。您对isValid() (return true;) 的首次实现将无法通过此测试,直到您实际实现长度验证。

标签: java unit-testing tdd


【解决方案1】:

或者我的方法从一开始就无效,我应该只为 isValid() 方法编写一个测试

是的,这个。

所以你写这样的东西作为起点:

public static boolean isValid(String passwordToValidate) {
    return false;
}

然后你可以编写你的第一个测试:

@Test
public void returnsTrueWhenPasswordIsAtLeastEightCharsLong() {
    assertTrue(isValid(EIGHT_CHARACTERS_LONG_PASSWORD));
}

哪个会失败:红色。

因此,您对被测代码进行最简单的更改以使其通过:

public static boolean isValid(String passwordToValidate) {
    return true;
}

那么测试将通过:GREEN。

但您尚未完全测试 8 个字符的限制:

@Test
public void returnsFalseWhenPasswordIsLessThanEightCharsLong() {
    assertFalse(isValid(SEVEN_CHARACTERS_LONG_PASSWORD));
}

这将失败 -- RED -- 因为被测方法现在总是返回 true。

因此,您只需进行最小的更改即可使所有测试通过:

public static boolean isValid(String passwordToValidate) {
    return passwordToValidate.length() >= 8;
}

你又变绿了。

然后您就这样继续,添加其他测试,并且每次尽可能少地更改您正在测试的代码以使测试通过。

您可能会发现在某个阶段重构您的 isValid 方法很有用,方法是将其拆分为您调用的一些私有方法。没关系:它是 refactor 步骤的一部分,并且您的测试应该仍然通过,因为公共接口没有改变。

【讨论】:

    【解决方案2】:

    这里的关键点是您缺少关于创建表示密码的抽象的主要概念,它为您提供了起点。由于缺少实体,您失去了焦点并以这种方式编写测试用例。 您想表示一个密码,因此您应该有一个对该实体进行建模的类,就域驱动设计而言,它是一个值对象。密码一旦创建,它实际上代表了它建模的概念,所以我只有两个测试,并且会在构造函数中逐步添加检查守卫

    testCreatesInvalidPassword() {
    // catch exception
    Password.fromString("asda");
    Password.fromString("asdas1");
    Password.fromString("asdas1...");
    }
    
    testCreatesValidPassword() {
    Password.fromString("asdasdsad");
    Password.fromString("asdas123");
    Password.fromString("asdas123");
    }
    

    【讨论】:

      【解决方案3】:

      您可能忽略的 Red -> Green -> Refactor 循环的一部分是 Refactor 步骤。此步骤也应适用于您的测试。因此,如果您发现测试之间存在重复,您应该将其重构出来,如果这意味着删除测试,那么您应该确信不再需要该测试。

      【讨论】:

        猜你喜欢
        • 2011-03-14
        • 2023-03-05
        • 1970-01-01
        • 2019-08-26
        • 2010-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多