如何在JUnit测试中避免多个断言?

时间:2011-02-17 05:33:10

标签: unit-testing junit

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

7 个答案:

答案 0 :(得分:7)

将它们分开。单元测试应该告诉你哪个单元失败了。保持它们分离还允许您快速隔离问题,而不需要您经历漫长的调试周期。

答案 1 :(得分:6)

真的建议only one assert per unit test吗?是的,有人提出这个建议。他们是对的吗?我不这么认为。我发现很难相信这些人已经在很长一段时间内真正开发过真正的代码了。

所以,imangine你有一个想要进行单元测试的mutator方法。 mutator有一些你要检查的效果或效果。通常,突变体的预期效果很少,因为许多效应表明突变体的设计过于复杂。每个效果有一个断言,每个断言有一个测试用例,每个mutator不需要很多测试用例,因此建议看起来并不那么糟糕。

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

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

那么,只需添加更多测试用例?在实践中,不变量的数量乘以要测试的变换器的数量很大,因此每次测试的一个断言导致许多测试用例。有关不变量的信息分散在许多测试用例中。调整一个不变量的设计更改将需要更改许多测试用例。这变得不切实际。最好为mutator设置参数化测试用例,使用多个断言检查mutator的几个不变量。

JUnit5的作者似乎同意。它们提供assertAll来检查一个测试用例中的几个断言。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

此构造可帮助您拥有1个大断言(内部有小断言)

import static org.junit.jupiter.api.Assertions.assertAll;

assertAll(
    () -> assertThat(actual1, is(expected)),
    () -> assertThat(actual2, is(expected))
);

答案 4 :(得分:0)

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

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

现在我对确切的语法并没有那么大,但这只是我正在看的概念。

编辑: 评论后......

答案是...... 没有!

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

所以你可以用两种方式之一:

  1. 一种方法,更少的样板代码。
  2. 许多方法,更好地报告测试运行
  3. 这取决于你,两者都有起伏。  3.列出项目

答案 5 :(得分:0)

[警告:我在Java / JUnit中非常“不流利”,所以要注意下面细节中的错误]

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

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

2)写一个帮助器断言,例如assertDtoFieldsEqual,传入预期的和实际的DTO。在帮助程序断言中,您分别断言每个字段。这至少为您提供了每次测试只有一个断言的错觉,并且如果您针对多种情况测试DTO生成,将使事情更清晰。

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

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(); }
  ...
}

答案 6 :(得分:-1)

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

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);
         }
     }  
}  

也许不是最好的方法,如果过度使用而不是混淆......但这是一种可能性。