【问题标题】:How to avoid multiple asserts in a JUnit test?如何在 JUnit 测试中避免多个断言?
【发布时间】:2011-06-28 20:48:46
【问题描述】:

我有一个从请求对象填充的 DTO,并且请求对象有很多字段。我想编写一个测试来检查 populateDTO() 方法是否将值放在正确的位置。如果我遵循每个测试一个断言的规则,我将不得不编写大量测试来测试每个字段。另一种方法是在单个测试中编写多个断言。是否真的建议每个测试规则遵循一个断言,或者我们可以在这些情况下放松。我该如何解决这个问题?

【问题讨论】:

    标签: unit-testing junit


    【解决方案1】:

    将它们分开。单元测试应该告诉你哪个单元失败了。将它们分开还可以让您快速隔离问题,而无需经历漫长的调试周期。

    【讨论】:

    • 断开的链接 - 请修改并告诉我
    • 链接仍然断开。这就是为什么最好内联您的答案并使用链接以获得支持,而不是提供链接作为答案的主要部分。
    • 修复了断开的链接。很抱歉给您带来不便。
    • 还是坏掉了,Nilesh。也许将文章转换为 PDF 并从 Google Drive 等链接?
    • 我不同意你的观点,因为 JUnit 允许你去到失败的断言,所以即使有 20 个断言一个接一个也没有任何区别,你可能只是添加如果您编写一个函数来单独测试类的每个成员,则处理时间。在我看来,每个测试的一个断言是一个神话,并且确实被高估/过度概括了“推荐”。但这只是我的看法。
    【解决方案2】:

    真的推荐only one assert per unit test吗?是的,有人提出这个建议。他们是对的吗?我不这么认为。我很难相信这些人实际上已经在真正的代码上工作了很长时间。

    所以,想象一下你有一个想要进行单元测试的 mutator 方法。 mutator 有某种效果,或者你想要检查的效果。通常,mutator 的预期效果数量很少,因为许多效果表明 mutator 的设计过于复杂。每个效果一个断言,每个断言一个测试用例,每个 mutator 不需要很多测试用例,所以这个建议看起来还不错。

    但这种推理的缺陷在于,这些测试只关注 mutator 的预期效果。但是,如果 mutator 中有错误,它可能会产生意想不到的错误副作用。测试做出了一个愚蠢的假设,即代码没有一整类错误,并且未来的重构不会引入此类错误。最初编写该方法时,作者可能很明显不可能产生特定的副作用,但重构和添加新功能可能会使这种副作用成为可能。

    测试长寿命代码的唯一安全方法是检查突变体没有意外的副作用。但是你怎么能测试这些呢?大多数类都有一些不变量:任何mutator都无法改变的东西。例如,容器的size 方法永远不会返回负值。实际上,每个不变量都是每个 mutator(以及构造函数)的后置条件。每个 mutator 通常还具有一组不变量,用于描述它会进行哪些更改。例如,sort 方法不会更改容器的长度。类和 mutator 不变量实际上是 every mutator 调用的后置条件。为所有它们添加断言是检查意外副作用的唯一方法。

    那么,只是添加更多测试用例?在实践中,不变量的数量乘以要测试的变异器的数量很大,因此每个测试一个断言会导致许多测试用例。并且关于您的不变量的信息分散在许多测试用例中。调整一个不变量的设计更改将需要更改许多测试用例。它变得不切实际。最好为 mutator 提供参数化的测试用例,它使用多个断言检查 mutator 的多个不变量。

    JUnit5 的作者似乎也同意这一点。他们提供了一个assertAll 来检查一个测试用例中的多个断言。

    【讨论】:

      【解决方案3】:

      这个结构可以帮助你拥有 1 个大断言(里面有小断言)

      import static org.junit.jupiter.api.Assertions.assertAll;
      
      assertAll(
          () -> assertThat(actual1, is(expected)),
          () -> assertThat(actual2, is(expected))
      );
      

      【讨论】:

        【解决方案4】:

        您可以有一个parameterized test,其中第一个参数是属性名称,第二个参数是预期值。

        【讨论】:

          【解决方案5】:

          该规则是否扩展到循环中?考虑一下这个

          Collection expectedValues = // populate expected values
          populateDTO();
          for(DTO dto : myDtoContainer) 
            assert_equal(dto, expectedValues.get(someIndexRelatedToDto))
          

          现在我对确切的语法不是很关心,但这只是我正在研究的概念。

          编辑: 在cmets之后...

          答案是……不!

          该原则存在的原因是您可以识别对象的哪些部分失败。如果你在一种方法中使用它们,你只会遇到一个断言,然后是下一个,然后是下一个,你不会看到所有的。

          所以你可以有两种方法之一:

          1. 一种方法,更少的样板代码。
          2. 多种方法,更好地报告测试运行

          这取决于你,两者都有起起落落。 3. 列表项

          【讨论】:

          • DTO 是 POJO..请求也是 POJO。所以断言很可能看起来像 assertEqual("abc", dto.getName());等等。
          • 所以你想避免只拥有assert(dto.equals(mockObject),这就是你的意思吗?您想在不违反 1-per 原则的情况下断言每个单独的元素吗?
          • 是的,我想断言每个单独的元素,而不违反 1-per 原则。如您所知,请求是 JAXB 生成的对象。
          【解决方案6】:

          [警告:我在 Java/JUnit 中非常“不流利”,因此请注意以下详细信息中的错误]

          有几种方法可以做到这一点:

          1) 在同一个测试中编写多个断言。如果您只测试一次 DTO 生成,这应该没问题。您可以从这里开始,当这开始受到伤害时,您可以转向另一个解决方案。

          2) 编写辅助断言,例如assertDtoFieldsEqual,传入预期的和实际的 DTO。在辅助断言中,您分别断言每个字段。这至少会让您产生每次测试只有一个断言的错觉,并且如果您针对多种场景测试 DTO 生成,事情就会变得更加清晰。

          3) 为检查每个属性的对象实现 equals 并实现 toString 以便您至少可以手动检查断言结果以找出不正确的部分。

          4) 对于每个生成 DTO 的场景,创建一个单独的测试夹具,用于生成 DTO 并在 setUp 方法中初始化预期的属性。创建一个单独的测试来测试每个属性。这也导致了很多测试,但它们至少是单行的。伪代码示例:

          public class WithDtoGeneratedFromXxx : TestFixture
          {
            DTO dto = null;
          
            public void setUp()
            {
              dto = GenerateDtoFromXxx();
              expectedProp1 = "";
              ...
            }
          
            void testProp1IsGeneratedCorrectly()
            {
              assertEqual(expectedProp1, dto.prop1);
            }
            ...
          }
          

          如果您需要在不同场景下测试 DTO 生成并选择最后一种方法,那么编写所有这些测试很快就会变得乏味。如果是这种情况,您可以实现一个抽象的基本夹具,省略有关如何创建 DTO 和设置派生类的预期属性的详细信息。伪代码:

          abstract class AbstractDtoTest : TestFixture
          {
            DTO dto;
            SomeType expectedProp1;
          
            abstract DTO createDto();
            abstract SomeType getExpectedProp1();
          
            void setUp()
            {
              dto = createDto();
              ...
            }
          
            void testProp1IsGeneratedCorrectly()
            {
              assertEqual(getExpectedProp1(), dto.prop1);
            }
            ...
          }
          
          
          class WithDtoGeneratedFromXxx : AbstractDtoTest
          {
            DTO createDto() { return GenerateDtoFromXxx(); }
            abstract SomeType getExpectedProp1() { return new SomeType(); }
            ...
          }
          

          【讨论】:

          • 如果您想要一个函数来断言所有属性字段,您可能需要查看 hamcrest 类 SamePropertyValuesAs。您可以在 Assert.assertThat(returnedDto, samePropertyValuesAs(expectedDto)) 中使用它。 (即使有更多字段不正确,每次测试仍然只会出现 1 个错误。)
          【解决方案7】:

          或者你可以做一些解决方法。

          import junit.framework.Assert;
          import org.junit.After;
          import org.junit.AfterClass;
          import org.junit.Before;
          import org.junit.BeforeClass;
          import org.junit.Test;
          
          public class NewEmptyJUnitTest {
          
              public NewEmptyJUnitTest() {
              }
          
              @BeforeClass
              public static void setUpClass() throws Exception {
              }
          
              @AfterClass
              public static void tearDownClass() throws Exception {
              }
          
              @Before
              public void setUp() {
              }
          
              @After
              public void tearDown() {
              }
          
          
               @Test
               public void checkMultipleValues() {
                   String errMessages = new String();
          
                   try{
                       this.checkProperty1("someActualResult", "someExpectedResult");
                   } catch (Exception e){
                       errMessages += e.getMessage();
                   }
          
                  try{
                      this.checkProperty2("someActualResult", "someExpectedResult");
                   } catch (Exception e){
                       errMessages += e.getMessage();
                   }
          
                  try{
                       this.checkProperty3("someActualResult", "someExpectedResult");
                   } catch (Exception e){
                       errMessages += e.getMessage();
                   }
          
                  Assert.assertTrue(errMessages, errMessages.isEmpty());
          
          
               }
          
          
          
               private boolean checkProperty1(String propertyValue, String expectedvalue) throws Exception{
                   if(propertyValue == expectedvalue){
                       return true;
                   }else {
                       throw new Exception("Property1 has value: " + propertyValue + ", expected: " + expectedvalue);
                   }
               }
          
                 private boolean checkProperty2(String propertyValue, String expectedvalue) throws Exception{
                   if(propertyValue == expectedvalue){
                       return true;
                   }else {
                       throw new Exception("Property2 has value: " + propertyValue + ", expected: " + expectedvalue);
                   }
               }
          
                   private boolean checkProperty3(String propertyValue, String expectedvalue) throws Exception{
                   if(propertyValue == expectedvalue){
                       return true;
                   }else {
                       throw new Exception("Property3 has value: " + propertyValue + ", expected: " + expectedvalue);
                   }
               }  
          }  
          

          也许不是最好的方法,如果过度使用可能会造成混淆......但这是可能的。

          【讨论】:

          • 我想说任何使用都是过度使用。所有这些样板只是为了绕过“每个测试的单个断言”?
          • @merryprankster 嗯,是的,如果您没有为您执行此操作的工具,那么您可以自己编写。一旦你发现越来越多地使用它,你就可以制作这个通用实用程序。在某些时候,您可能会将其添加到您的测试框架中:github.com/KentBeck/junit/blob/r4.8.2/src/main/java/org/junit/…
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-06-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-24
          • 1970-01-01
          相关资源
          最近更新 更多