【问题标题】:Lombok @NonNull annotation with @Builder not reflecting in test coverage带有@Builder 的Lombok @NonNull 注释未反映在测试覆盖率中
【发布时间】:2020-07-27 23:21:35
【问题描述】:

我在测试 lombok 带注释的类时遇到了一个问题。 说明:-

当我们用@Builder注解一个java类(POJO),并且有一个带有@NonNull约束的实例变量时,在编写测试用例检查NullPointerException时,我们不能实例化POJO 与 builder 并期望它会根据 null 检查进行评估。我对类进行了 deomboked,发现非 null 约束在生成的 builder 类本身内部流动,这意味着甚至在我们构造对象之前就抛出了一个空指针异常。这是一个在某种意义上是正确的行为,但我需要知道我是否可以测试这样的场景。

@Builder
public class Sample {
    @NonNull
    private final String a;
    private final String b;
}

现在我需要测试在“a”为 Null 的情况下引发 NullPointerException 的情况。 对于这种情况,我有 2 个选择:

  • 我可以使用构造函数创建示例类 - new Sample(null, null)
  • 或者我可以通过构建器创建 - Sample.builder.a(null).build();

我的问题在第二部分,因为第一个可以正常工作。 当我delombok时,我会得到类似的东西:-

public class Sample {
    @NonNull
    private final String a;
    private final String b;

    Sample(@NonNull String a, String b) {
        this.a = a;
        this.b = b;
    }

    public static SampleBuilder builder() {
        return new SampleBuilder();
    }

    public static class SampleBuilder {
        private @NonNull String a;
        private String b;

        SampleBuilder() {
        }

        public SampleBuilder a(@NonNull String a) {
            this.a = a;
            return this;
        }

        public SampleBuilder b(String b) {
            this.b = b;
            return this;
        }

        public Sample build() {
            return new Sample(a, b);
        }

        public String toString() {
            return "Sample.SampleBuilder(a=" + this.a + ", b=" + this.b + ")";
        }
    }
}

如果您看到 NullPointerException 将在 SampleBuilder 内部抛出,因为它需要 @NonNull 参数,并且构造函数将永远不会执行条件来检查非空属性,因为测试覆盖率会下降。如果我们使用 @SuperBuilder ,这不会发生,因为它不会在构建器参数中使用 @NonNull

【问题讨论】:

    标签: java lombok builder-pattern


    【解决方案1】:

    不太确定你在期待什么,但下面的源代码可以正确测试 Sample 类的构造函数和生成器的 null 检查。

    您可以在https://repl.it/repls/CleverWiryTruespace 看到它正在运行。

    import lombok.NonNull;
    import lombok.Builder;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.Before;
    import org.junit.rules.ExpectedException;
    import org.junit.runner.JUnitCore;
    
    class Main {
      @Builder
      public static class Sample {
          @NonNull
          private final String a;
          private final String b;
      }
    
      public static class TestSample {
        @Rule
        public ExpectedException thrown = ExpectedException.none();
    
        @Before
        public void setUp() {
          thrown.expect(NullPointerException.class);
          thrown.expectMessage("a is marked non-null but is null");
        }
    
        @Test
        public void test1() {
          Sample s = new Sample(null, "abc");
        }
    
        @Test
        public void test2() {
          Sample.builder().a(null).build();
        }
      }
    
      public static void main(String[] args) {
        new JUnitCore().main(TestSample.class.getName());
      }
    }
    

    【讨论】:

    • 让我用您的测试用例示例本身澄清一下。我想要实现的是通过构建器创建对象来在构造器级别获取 NullPointerException。
    • 在您提到的上述示例 (test2) 中,NullPointerException 将被抛出,但在 SampleBuilder 方法级别,这意味着我无法覆盖 lombok 为 @NonNull 构造函数参数生成的分支覆盖行,其中显示类似于 if (a ==null),抛出NPE。我可以通过不设置参数'a'来实现这一点。我可以用以下方式编写test2() - Sample.builder().build();这不会在构建器级别而是在构造器级别触发 NPE。
    • 是的,这是完全可以预料的,因为不是构建器模式的问题。它不打算在调用构建时进行预验证。那将是多余的。此外,构建器模式将调用一个构造器,它甚至可以有额外的约束。
    • 我认为您可能试图获得 100% 的覆盖率,但这是不正确的。您应该将那些 lombok 生成的方法从您的覆盖范围中过滤掉。 100% 覆盖应该意味着覆盖 100% 的代码,而不是由第三方库生成的东西。你需要信任外部依赖提供的代码。
    【解决方案2】:

    从代码中我们可以看到,如果我们想在构造函数级别触发 NPE,我们需要将 @NonNull 参数保留为空,而不是将其设置为 null,这将触发构建器方法级别的 NPE。 如果我以下列方式构建 Sample 对象,它应该就足够了:-

    Sample sample = Sample.builder.b("b").build();
    

    如果我想在构造函数级别测试 'a' 的 NPE,我没有将参数 'a' 显式设置为 NULL。

    【讨论】:

      猜你喜欢
      • 2019-09-24
      • 2018-01-13
      • 2022-12-03
      • 2016-04-07
      • 1970-01-01
      • 2016-03-27
      • 1970-01-01
      • 2012-05-03
      • 2019-10-16
      相关资源
      最近更新 更多