开发罪过_七大罪过与如何避免

开发罪过

在整个本文中,我将在代码片段中使用Java,同时还将使用JUnitMockito

本文旨在提供以下测试代码示例:

  • 难以阅读
  • 难以维护

在这些示例之后,本文将尝试提供替代方法,这些替代方法可用于增强测试的可读性,从而有助于使其在将来更易于维护。

创建良好的示例具有挑战性,因此,作为读者,我鼓励您将示例仅用作了解本文基本信息的工具,以力求实现可读的测试代码。

1.通用测试名称

您可能已经看到了如下所示的测试

@Test
void testTranslator() {
    String word = new Translator().wordFrom(1);

    assertThat(word, is("one"));
}

现在这是非常通用的,不会通知代码的读者该测试实际在测试什么。 Translator可能有多种方法,我们如何知道测试中正在使用哪种方法? 通过查看测试名称并不清楚,这意味着我们必须查看测试本身才能看到。

我们可以做得更好,因此可以看到以下内容:

@Test
void translate_from_number_to_word() {
    String word = new Translator().wordFrom(1);

    assertThat(word, is("one"));
}

从上面的内容可以看出,它在解释此测试的实际作用方面做得更好。 此外,如果您将测试文件命名为TranslatorShould那么在将测试文件和单个测试名称组合在一起时,您应该在头脑中形成一个合理的句子: Translator should translate from number to word

2.测试设置中的变异

在测试中,您很有可能希望将测试中使用的对象构造为处于特定状态。 有不同的方法,下面显示了一种这样的方法。 在此代码段中,我们基于该对象中包含的信息来确定某个字符是否实际上是“ Luke Skywalker”(想象这就是isLuke()方法的作用):

@Test
void inform_when_character_is_luke_skywalker() {
    StarWarsTrivia trivia = new StarWarsTrivia();
    Character luke = new Character();
    luke.setName("Luke Skywalker");
    Character vader = new Character();
    vader.setName("Darth Vader");
    luke.setFather(vader);
    luke.setProfession(PROFESSION.JEDI);

    boolean isLuke = trivia.isLuke(luke);

    assertTrue(isLuke);
}

上面的代码构造了一个Character对象来表示“ Luke Skywalker”,此后发生的事涉及相当比例的突变。 它继续在随后的行中设置名称,父母身份和职业。 当然,这忽略了与我们的朋友“达斯·维达”发生的类似事情。

这种突变水平分散了测试中正在发生的事情。 如果我们再回顾一下我先前的句子:

在测试中很有可能您希望将测试中使用的对象构造为处于特定状态

但是,上述测试实际上发生了两个阶段:

  • 构造对象
  • 使其处于某种状态

这是不必要的,我们可以避免。 可能有人建议,为了避免发生突变,我们可以简单地将所有内容都移植并转储到构造函数中,以确保我们以给定的状态构造对象,避免发生突变:

@Test
void inform_when_character_is_luke_skywalker() {
    StarWarsTrivia trivia = new StarWarsTrivia();
    Character vader = new Character("Darth Vader");
    Character luke = new Character("Luke Skywalker", vader, PROFESSION.JEDI);

    boolean isLuke = trivia.isLuke(luke);

    assertTrue(isLuke);
}

从上面可以看到,我们减少了代码行的数量以及对象的变异。 但是,在此过程中,我们已经失去了Character (现在为Character参数)在测试中表示的含义。 为了使isLuke()方法返回true,我们传入的Character对象必须具有以下内容:

  • “卢克·天行者”的名字
  • 有一个父亲叫“达斯·维达”
  • 成为绝地武士

但是,从这种情况的测试中尚不清楚,我们必须检查Character的内部以了解这些参数的用途(否则您的IDE会告诉您)。

我们可以做得更好,可以利用Builder模式在所需状态下构造一个Character对象,同时还可以保持测试的可读性:

@Test
void inform_when_character_is_luke_skywalker() {
    StarWarsTrivia trivia = new StarWarsTrivia();
    Character luke = CharacterBuilder().aCharacter()
        .withNameOf("Luke Skywalker")
        .sonOf(new Character("Darth Vader"))
        .employedAsA(PROFESSION.JEDI)
        .build();

    boolean isLuke = trivia.isLuke(luke);

    assertTrue(isLuke);
}

通过上面的内容,可能还会有几行内容,但是它试图解释测试中的重要内容。

3.断言疯狂

在测试期间,您将断言/验证系统中是否发生了某些事情(通常位于每次测试结束时)。 这是测试中非常重要的一步,可能很想添加许多断言,例如断言返回对象的值。

@Test
void successfully_upgrades_user() {
    UserService service = new UserService();
    User someBasicUser = UserBuilder.aUser()
        .withName("Basic Bob")
        .withAge(23)
        .withTypeOf(UserType.BASIC)
        .build();

    User upgradedUser = service.upgrade(someBasicUser);

    assertThat(upgradedUser.name(), is("Basic Bob"));
    assertThat(upgradedUser.type(), is(UserType.SUPER_USER));
    assertThat(upgradedUser.age(), is(23));
}

(在上面的示例中,我向构建器提供了其他信息,例如名称和年龄,但是,如果对测试不重要,则通常不会包含此信息,请在构建器中使用明智的默认值)

如我们所见,存在三个断言,在更极端的示例中,我们谈论的是数十行断言。 我们不一定需要执行三个断言,有时我们可以合而为一:

@Test
void successfully_upgrades_user() {
    UserService service = new UserService();
    User someBasicUser = UserBuilder.aUser()
        .withName("Basic Bob")
        .withAge(23)
        .withTypeOf(UserType.BASIC)
        .build();

    User expectedUserAfterUpgrading = UserBuilder.aUser()
        .withName("Basic Bob")
        .withAge(23)
        .withTypeOf(UserType.SUPER_USER)
        .build();


    User upgradedUser = service.upgrade(someBasicUser);

    assertThat(upgradedUser, is(expectedUserAfterUpgrading));
}

现在,我们将升级后的用户与我们期望对象在升级后的外观进行比较。 为此,您将需要比较的对象( User )具有覆盖的equalshashCode

4.神奇的价值观

您是否曾经看过数字或字符串并想知道它代表什么? 我已经拥有了那些不得不解析代码行的宝贵时间,这些时间很快就会开始累加起来。 我们在下面有一个这样的代码示例。

@Test
void denies_entry_for_someone_who_is_not_old_enough() {
    Person youngPerson = PersonBuilder.aPerson()
        .withAgeOf(17)
        .build();

    NightclubService service = new NightclubService(21);

    String decision = service.entryDecisionFor(youngPerson);

    assertThat(decision, is("No entry. They are not old enough."));
}

阅读以上内容,您可能会遇到一些问题,例如:

  • 17是什么意思?
  • 21在构造函数中是什么意思?

如果我们可以向代码读者表示它们的含义,那不是很好,那么他们不必考虑太多吗? 幸运的是,我们可以:

private static final int SEVENTEEN_YEARS = 17;
private static final int MINIMUM_AGE_FOR_ENTRY = 21;
private static final String NO_ENTRY_MESSAGE = "No entry. They are not old enough.";

@Test
void denies_entry_for_someone_who_is_not_old_enough() {
    Person youngPerson = PersonBuilder.aPerson()
        .withAgeOf(SEVENTEEN_YEARS)
        .build();

    NightclubService service = new NightclubService(MINIMUM_AGE_FOR_ENTRY);

    String decision = service.entryDecisionFor(youngPerson);

    assertThat(decision, is(NO_ENTRY_MESSAGE));
}

现在,当我们看以上内容时,我们知道:

  • SEVENTEEN_YEARS是用来表示17年的值,毫无疑问,我们已经在读者的脑海中留下了疑问。 不是秒或分钟,而是年。
  • MINIMUM_AGE_FOR_ENTRY是必须允许某人进入夜总会的值。 读者甚至不必关心此值是什么,只需了解测试上下文中的含义即可。
  • NO_ENTRY_MESSAGE是返回的值,表示不允许某人进入夜总会。 从本质上讲,字符串通常具有更好的描述性,但是请始终检查您的代码以找出可以改进的地方。

这里的关键是减少代码阅读器尝试解析代码行所花费的时间。

5.难以读取的测试名称

@Test
void testingNumberOneAndNumberTwoCanBeAddedTogetherToProduceNumberThree() {
    ...
}

您花了多长时间阅读以上内容? 它易于阅读吗?您能快速了解一下此处正在测试的内容吗?还是需要解析许多字符?

幸运的是,我们可以尝试以更好的方式命名测试,方法是将测试减少到实际测试的水平,并删除试图添加的华夫饼:

@Test
void twoNumbersCanBeAdded() {
    ...
}

它的阅读效果更好吗? 我们减少了这里的单词数量,更易于解析。 如果我们可以更进一步,问我们是否可以放弃使用骆驼箱怎么办:

@Test
void two_numbers_can_be_added() {
    ...
}

这是一个优先事项,应该由对给定代码库做出贡献的人员同意。 使用蛇形小写字母(如上所述)可以帮助提高测试名称的可读性,因为您更可能打算模仿书面句子。 因此,蛇形格的使用紧随普通书面句子中存在的物理空间。 但是,Java不允许在方法名称中使用空格,这是我们所拥有的最好的方法,缺少使用Spock之类的东西。

6.依赖项注入的设置器

通常,对于测试,您希望能够为给定对象(也称为“协作对象”或简称为“协作者”)注入依赖关系。 为了达到这个目的,您可能已经看到了类似以下内容的内容:

@Test
void save_a_product() {
    ProductService service = new ProductService();
    TestableProductRepository repository = mock(TestableProductRepository.class);
    service.setRepository(repository);
    Product newProduct = new Product("some product");

    service.addProduct(newProduct);

    verify(repository).save(newProduct);
}

上面使用了setter方法,即setRepository() ,以便注入TestableProductRepository的模拟,因此我们可以验证服务和存储库之间是否发生了正确的协作。

与围绕突变的点类似,这里我们对ProductService进行突变,而不是将其构造为所需状态。 可以通过将协作者注入构造函数中来避免这种情况:

@Test
void save_a_product() {
    TestableProductRepository repository = mock(TestableProductRepository.class);
    ProductService service = new ProductService(repository);
    Product newProduct = new Product("some product");

    service.addProduct(newProduct);

    verify(repository).save(newProduct);
}

因此,现在我们将协作者注入了构造函数中,现在我们在构造时就知道对象将处于什么状态。但是,您可能会问“在此过程中我们是否没有丢失某些上下文?”。

我们已经从

service.setRepository(repository);

ProductService service = new ProductService(repository);

前者更具描述性。 因此,如果您不喜欢这种上下文丢失的情况,则可以选择类似构建器的内容,而创建以下内容:

@Test
void save_a_product() {
    TestableProductRepository repository = mock(TestableProductRepository.class);
    ProductService service = ProductServiceBuilder.aProductService()
                                .withRepository(repository)
                                .build();
    Product newProduct = new Product("some product");

    service.addProduct(newProduct);

    verify(repository).save(newProduct);
}

该解决方案使我们能够避免在使用withRepository()方法记录协作者注入的情况下改变ProductService

7.非描述性验证

如前所述,您的测试通常会包含验证语句。 不用自己动手,您通常会利用库来执行此操作。 但是,您必须注意不要掩盖验证的意图。 要了解我在说什么,请看以下示例。

@Test
void no_error_is_shown_when_user_is_valid() {
    UIComponent component = mock(UIComponent.class);
    User user = mock(User.class);
    when(user.isValid()).thenReturn(true);
    LoginController controller = new LoginController();

    controller.attemptLogin(component, user);

    verifyZeroInteractions(component);
}

现在,如果您看上面的内容,您是否立即知道该断言表明没有错误显示给用户? 可能是因为它是测试的名称,但是您可能不将该代码行与测试名称相关联 。 这是因为它是Mockito的代码,并且通用以适应许多不同的用例。 它按照它说的做,检查与UIComponent的模拟是否没有交互。

但是,这意味着您的测试有所不同。 我们如何设法使其更加清晰。

@Test
void no_error_is_shown_when_user_is_valid() {
    UIComponent component = mock(UIComponent.class);
    User user = mock(User.class);
    when(user.isValid()).thenReturn(true);
    LoginController controller = new LoginController();

    controller.attemptLogin(component, user);

    verify(component, times(0)).addErrorMessage("Invalid user");
}

这样会更好一些,因为此代码的读者有很大的潜力可以快速了解此行的工作。 但是,在某些情况下,可能仍然很难阅读。 在这种情况下,请按照以下说明提取一种方法,以更好地解释您的验证。

@Test
void no_error_is_shown_when_user_is_valid() {
    UIComponent component = mock(UIComponent.class);
    User user = mock(User.class);
    when(user.isValid()).thenReturn(true);
    LoginController controller = new LoginController();

    controller.attemptLogin(component, user);

    verifyNoErrorMessageIsAddedTo(component);
}

private void verifyNoErrorMessageIsAddedTo(UIComponent component) {
    verify(component, times(0)).addErrorMessage("Invalid user");
}

上面的代码并不完美,但是在当前测试的范围内,它肯定可以提供我们正在验证的内容的高级概述。

结束语

我希望您喜欢这篇文章,并且下次您完成编写测试时将花费一两个重构步骤。 在下一次之前,我给你以下报价:

“必须编写程序供人们阅读,并且只能偶然地使机器执行。” ― Harold Abelson,计算机程序的结构和解释

翻译自: https://www.javacodegeeks.com/2019/08/seven-testing-sins-and-how-to-avoid-them.html

开发罪过

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值