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

发布于 2024-10-17 19:11:10 字数 176 浏览 2 评论 0原文

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

I have a DTO which I'm populating from the request object, and the request object has many fields. I want to write a test to check if the populateDTO() method is putting values in the right places or not. If I follow the rule of one assert per test, I would have to write a large number of tests, to test each field. The other approach would be to write multiple asserts in a single test. Is it really recommended to follow one assert per test rule or can we relax in these cases. How do I approach this problem?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

り繁华旳梦境 2024-10-24 19:11:10

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

Keep them separate. A unit test is supposed to tell you which unit failed. Keeping them separate also allows you to isolate the problem quickly w/o requiring you to go through a lengthy debug cycle.

冷血 2024-10-24 19:11:10

是否真的建议进行 每个单元测试只有一个断言?是的,确实有人提出了这样的建议。他们是对的吗?我不这么认为。我很难相信这些人实际上已经在真正的代码上工作了很长时间。

因此,想象一下您有一个想要进行单元测试的变异方法。变异器具有某种或多种效果,您需要检查这些效果。通常,增变器的预期效果数量很少,因为许多效果表明增变器的设计过于复杂。每个效果有一个断言,每个断言有一个测试用例,每个变异器不需要很多测试用例,因此建议看起来并没有那么糟糕。

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

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

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

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

Is it really recommended to have only one assert per unit test? Yes it is, there are people who make that recommendation. Are they right? I don't think so. I find it hard to believe such people have actually worked on real code for a long time.

So, imangine you have a mutator method you want to unit test. The mutator has some kind of effect, or effects, which you want to check. Typically the expected effect of a mutator are few in number, because many effects suggests an overly complicated design for the mutator. With one assert per effect and one test case per assert, you will not need many test cases per mutator, so the recommendation does not seem so bad.

But the flaw in this reasoning is that those tests are looking at only the expected effects of the mutator. But if the mutator has a bug in it, it might have unexpected faulty side effects. The tests are making the foolish assumption that the code does not have a whole class of bugs, and that no future refactoring will introduce such bugs. When the method was originally written it might be obvious to the author that particular side effects were impossible, but refactoring and addition of new functionality might make such side effects possible.

The only safe way to test long lived code is to check that the mutators do not have unexpected side effects. But how can you test for those? Most classes have some invariants: things that no mutator can ever change. The size method of a container will never return a negative value, for example. Each invariant is, in effect, a post condition for every mutator (and also the constructor). Each mutator also typically has a set of invariants that describe what kind of changes it does not make. A sort method does not change the length of the container, for example. The class and mutator invariants are, in effect, post conditions for every mutator call. Adding assertions for all them is the only way of checking for unexpected side effects.

So, just add more test cases? In practice the number of invariants multiplied by the number of mutators to test is large, so one assertion per test leads to many test cases. And the information about your invariants is scattered over many test cases. A design change to tweak one invariant will require alteration of many test cases. It becomes impractical. Its better to have parameterised test cases for a mutator, which check several invariants for the mutator, using several assertions.

And the authors of JUnit5 seem to agree. They provide an assertAll for checking several assertions in one test-case.

橘寄 2024-10-24 19:11:10

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

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

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

this construction help you to have 1 big assert (with small asserts inside)

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

assertAll(
    () -> assertThat(actual1, is(expected)),
    () -> assertThat(actual2, is(expected))
);
叫思念不要吵 2024-10-24 19:11:10

您可以进行参数化测试,其中第一个参数是属性名称第二个是期望值。

You can have a parameterized test where the 1st parameter is the propertyname and the second the expected value.

倾其所爱 2024-10-24 19:11:10

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

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

现在我对确切的语法不太感兴趣,但这只是我正在考虑的概念。

编辑:
评论后...

答案是...不!

该原理存在的原因是为了让您能够识别对象的哪些部分发生了故障。如果您在一种方法中拥有它们,那么您将只会遇到一个断言,然后是下一个断言,然后是下一个断言,并且您不会看到它们全部。

因此,您可以通过以下两种方式之一获得它:

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

取决于你,两者都有优点和缺点。
3. 列表项

Is that rule extended to being in a loop? Consider this

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

Now I'm not so big on the exact syntax, but this is just the notion I'm looking at.

EDIT:
After the comments...

The answer is ... Nope!

The reason the principle exists is so you can identify which parts of the object fail. If you have them in one method, you're going to run into only one assertion, then the next, then the next, and you won't see them all.

So you can have it one of two ways:

  1. One method, less boilerplate code.
  2. Many methods, better reporting on the test run

It's up to you, both have ups and downs.
3. List item

满意归宿 2024-10-24 19:11:10

[警告:我对 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(); }
  ...
}

[caveat: I'm very "unfluent" in Java/JUnit, so beware of errors in the details below]

There's a couple of ways to do this:

1) Write multiple assertions in the same test. This should be ok if you are only testing the DTO generation once. You could start here, and move to another solution when this starts to hurt.

2) Write a helper assertion, e.g. assertDtoFieldsEqual, passing in the expected and actual DTO. Inside the helper assertion you assert each field separately. This at least gives you the illusion of only one assert per test and will make things clearer if you test DTO generation for multiple scenarios.

3) Implement equals for the object that check each property and implement toString so that you at least can inspect the assertion result manually to find out what part is incorrect.

4) For each scenario where the DTO is generated, create a separate test fixture that generates the DTO and initializes the expected properties in the setUp method. The create a separate test for testing each of the properties. This also results in a lot of tests, but they will at least be one-liners only. Example in pseudo-code:

public class WithDtoGeneratedFromXxx : TestFixture
{
  DTO dto = null;

  public void setUp()
  {
    dto = GenerateDtoFromXxx();
    expectedProp1 = "";
    ...
  }

  void testProp1IsGeneratedCorrectly()
  {
    assertEqual(expectedProp1, dto.prop1);
  }
  ...
}

If you need to test the DTO generation under different scenarios and choose this last method it could soon become tedious to write all those tests. If this is the case you could implement an abstract base fixture that leaves out the details on how to create the DTO and setup the expected properties to derived classes. Pseudo-code:

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(); }
  ...
}
凶凌 2024-10-24 19:11:10

或者您可以采取一些解决方法。

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

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

Or you can do some workaround.

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

Maybe not the best approach and if overused than can confuse... but it is a possibility.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文