【问题标题】:JUnit avoid duplicate assertionsJUnit 避免重复断言
【发布时间】:2019-03-20 09:25:17
【问题描述】:

我正在编写将实体转换为 DTO 的简单测试用例,反之亦然。问题更多是关于设计。在下面的代码中留下重复项是否可以接受,或者为这个断言创建外部方法更好?由于我是 Java 新手,有人可以给我提示一下吗?任何通用方法?我不想为这种简单的实体及其 DTO 使用继承或任何其他抽象,因为将有更多的代码,而不仅仅是几行重复的代码。 这是它现在的样子:

@Test
void addressToAddressDTO() {
    Address address = getAddress();

    AddressDTO addressDTO = addressMapper.addressToAddressDTO(address);

    assertAll("Check if values were properly bound",
            () -> {
                assertEquals(address.getCity(), addressDTO.getCity());
                assertEquals(address.getUserDetails().getFirstName(), addressDTO.getUserDetails().getFirstName());
                assertEquals(address.getUserDetails().getUser().getUsername(), addressDTO.getUserDetails().getUser().getUsername());
                assertEquals(address.getUserDetails().getContact().getEmail(), addressDTO.getUserDetails().getContact().getEmail());
                assertEquals(address.getUserDetails().getProfileImage().getImageUrl(), addressDTO.getUserDetails().getProfileImage().getImageUrl());
            });
}

@Test
void addressDTOtoAddress() {
    AddressDTO addressDTO = getAddressDTO();

    Address address = addressMapper.addressDTOtoAddress(addressDTO);

    assertAll("Check if values were properly bound",
            () -> {
                assertEquals(addressDTO.getCity(), address.getCity());
                assertEquals(addressDTO.getUserDetails().getFirstName(), address.getUserDetails().getFirstName());
                assertEquals(addressDTO.getUserDetails().getUser().getUsername(), address.getUserDetails().getUser().getUsername());
                assertEquals(addressDTO.getUserDetails().getContact().getEmail(), address.getUserDetails().getContact().getEmail());
                assertEquals(addressDTO.getUserDetails().getProfileImage().getImageUrl(), address.getUserDetails().getProfileImage().getImageUrl());
            });
}

我的想法是创建更通用的东西,例如:

private<T, S> void assertObject(T expected, S actual) {
        assertAll("Check if values were properly bound",
                () -> {
                    assertEquals(expected.getCity(), actual.getCity());
                    assertEquals(expected.getUserDetails().getFirstName(), actual.getUserDetails().getFirstName());
                    assertEquals(expected.getUserDetails().getUser().getUsername(), actual.getUserDetails().getUser().getUsername());
                    assertEquals(expected.getUserDetails().getContact().getEmail(), actual.getUserDetails().getContact().getEmail());
                    assertEquals(expected.getUserDetails().getProfileImage().getImageUrl(), actual.getUserDetails().getProfileImage().getImageUrl());
                });
    }

但由于即使它们是相同的对象,它们也没有任何共同之处。如何实现地址和地址DTO既可以是实际的也可以是预期的hmm可互换的东西?

编辑

根据 Aaron Digulla 的回答,我做了一些更改,希望对有同样疑问的人有所帮助。如果有人知道任何其他选项,请在评论部分发表。

@Test
void addressToAddressDTO() {
    Address expected = getAddress();

    AddressDTO actual = addressMapper.addressToAddressDTO(expected);

    assertEquals(
            mergeAddressDataToString(expected),
            actual.getCity() + "," +
                    actual.getUserDetails().getFirstName() + "," +
                    actual.getUserDetails().getUser().getUsername() + "," +
                    actual.getUserDetails().getContact().getEmail() + "," +
                    actual.getUserDetails().getProfileImage().getImageUrl()

    );
}

@Test
void addressDTOtoAddress() {
    AddressDTO expected = getAddressDTO();

    Address actual = addressMapper.addressDTOtoAddress(expected);

    assertEquals(
            expected.getCity() + "," +
                    expected.getUserDetails().getFirstName() + "," +
                    expected.getUserDetails().getUser().getUsername() + "," +
                    expected.getUserDetails().getContact().getEmail() + "," +
                    expected.getUserDetails().getProfileImage().getImageUrl(),
            mergeAddressDataToString(actual)
    );
}

private String mergeAddressDataToString(Address address) {
    StringJoiner stringJoiner = new StringJoiner(",");
    stringJoiner.add(address.getCity());
    stringJoiner.add(address.getUserDetails().getFirstName());
    stringJoiner.add(address.getUserDetails().getUser().getUsername());
    stringJoiner.add(address.getUserDetails().getContact().getEmail());
    stringJoiner.add(address.getUserDetails().getProfileImage().getImageUrl());

    return stringJoiner.toString();
}

【问题讨论】:

    标签: unit-testing generics junit junit5


    【解决方案1】:

    当多个测试需要共享它时,我通常为测试中的对象或测试实用程序类编写自定义toString() 方法。这样,我可以使用单个 assertEquals() 检查所有值。

    我可以使用换行符使断言在 IDE 中更具可读性:

    assertEquals(
        "city=Foo\n" + 
        "firstName=John\n" + 
        "user=doe\n" + 
        ....
        , toString(actual));
    

    当您必须检查值列表时也可以很好地工作:

        ...
        , list.stream().map(this::toString).collect(Collectors.joining("\n---\n"));
    

    这里最大的优势是你可以一次得到所有的不匹配。您还可以调整 toString() 方法来处理极端情况(例如仅比较大型列表的元素数量或舍入小数)。

    它还使代码易于理解。在许多情况下,它还可以节省您编写代码以填充您需要的所有 expected 对象的时间。当多个测试应该产生相同的结果时,测试甚至可以共享预期的字符串。

    当测试因为输出改变而中断时,我可以选择整个字符串并将其替换为新的输出。 IDE 将为我进行格式化。

    【讨论】:

    • 我一定会尝试这种方法!
    【解决方案2】:

    除了重复之外,您的初始方法使用一个反模式:您调用assertAll,但仍将所有断言放在一个块中。因此,在第一个失败的断言之后,块的执行将被终止。相反,如果您将每个单独的检查放在一个 Executable 中,则在失败的情况下,将执行所有检查,您将获得有关失败和失败的更多详细信息。当然,这不再是字符串比较方法的问题了。

    关于重复,还有一个想法是在这种特殊情况下如何避免它,即对于两个转换函数的测试:您可以利用在两种类型之间来回转换的事实是恒等函数:

    Address address = getAddress();
    AddressDTO addressDTO = addressMapper.addressToAddressDTO(address);
    Address actual = addressMapper.addressDTOtoAddress(addressDTO);
    assertEquals(address, actual);
    

    这消除了单个元素的比较。如果实体的表示和 DTO 以不再严格相等的方式更改,而只是必须可以来回转换,这甚至可能是有利的。

    但是,现在每个测试还依赖于被测类的其他方法。这通常不是问题:例如,许多测试依赖于构造函数来工作 - 如果也有构造函数的测试,那就可以了。然而,在这里,如果测试失败,有两个可能的位置负责,因此要找到罪魁祸首需要更多的分析。

    关于您尝试过的第二个选项,即创建字符串并比较它们:在某些情况下,这种方法可能会有所帮助,但通常我对从结构化数据到字符串犹豫不决。假设您使用该模式创建了很多测试。但是,稍后您会意识到,在 DTO 中,某些属性的编码方式必须与实体中的不同(我在上面提到过这种情况)。突然,转换为字符串不再起作用或变得尴尬。

    使用或不使用字符串方法:如果您有更多的断言需要比较完整的实体对象和 DTO,我建议您编写自己的断言辅助方法,例如 assertEntityMatchesDtoassertDtoMatchesEntity

    最后一句话:可能有理由不在一个测试中比较对象的所有属性:这样,测试可能不够集中。再次以编码更改为例:假设您必须在某个时间点更改 DTO 中电子邮件地址的表示。然后,如果您总是查看测试中的所有属性,那么您的所有测试都会失败。相反,如果您针对单个属性进行了集中测试,则此类更改(如果正确完成)只会影响专注于电子邮件属性的测试,而不会影响其他测试。

    【讨论】:

      猜你喜欢
      • 2011-06-28
      • 2023-03-15
      • 2015-01-10
      • 1970-01-01
      • 2021-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多