【问题标题】:Lombok @Synchronized with Mockito throws NPELombok @Synchronized 与 Mockito 抛出 NPE
【发布时间】:2020-08-28 04:13:23
【问题描述】:

鉴于synchronized 和Lombok 的@Synchronized,后者在模拟被测方法时会导致NullPointerException。给定

public class Problem
{
    public Problem()
    {
        // Expensive initialization,
        // so use Mock, not Spy
    }

    public synchronized String a()
    {
        return "a";
    }

    @Synchronized // <-- Causes NPE during tests, literally, here
    public String b()
    {
        return "b";
    }
}

和木星测试类

class ProblemTest
{
    @Mock
    private Problem subject;

    @BeforeEach
    void setup()
    {
        initMocks(this);
        // There is more mocking. Please don't let the simplicity
        // of this example throw you off.
        doCallRealMethod().when( subject ).a();
        doCallRealMethod().when( subject ).b();

        // This is a hack, but works. Can we rely on this?
        // ReflectionTestUtils.setField( subject, "$lock", new Object[0] );
    }

    @Test
    void a()
    {
        // Succeeds
        assertEquals( "a", subject.a() );
    }

    @Test
    void b()
    {
        // NullPointerException during tests
        assertEquals( "b", subject.b() );
    }
}

Lombok 添加如下内容:

private final Object $lock = new Object[0]; // We can't rely on this name
...
public String b()
{
    synchronized($lock)
    {
        return "b";
    }
}

如何模拟一个用 Lombok 的 default @Synchronized 注解修饰的方法?


这是堆栈跟踪,虽然它没有帮助。我怀疑 Lombok 在上面的示例中添加了一个字段,当然这并没有注入到模拟中,所以瞧,NPE。

java.lang.NullPointerException
    at com.ericdraken.Problem.b(Problem.java:16) // <-- @Synchronized keyword
    at com.ericdraken.ProblemTest.b(ProblemTest.java:43) // <-- assertEquals( "b", subject.b() );
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    ... [snip] ...
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

【问题讨论】:

  • 请显示异常堆栈跟踪。
  • 它不会帮助你,因为它在 @Synchronized 关键字处是 NPE,但我可以将它添加到问题中。

标签: java mockito junit5 synchronized lombok


【解决方案1】:

这不是 Lombok 的问题,以下也失败了。

@ExtendWith({MockitoExtension.class})
@MockitoSettings(strictness = Strictness.LENIENT)
public class ProblemTest {
    @Mock
    private Problem subject;

    @BeforeEach
    void setup()
    {
        doCallRealMethod().when( subject ).c();

    }

    @Test
    void c()
    {
        // NullPointerException during tests
        assertEquals( "c", subject.c() );
    }
}

class Problem
{
  private final Map<String,String> c = new HashMap<>(){{put("c","c");}};

    public String c(){
        return c.get("c");
    }
}

确切地说,您并不是真的在嘲笑 Problem,而是通过 doCallRealMethod部分地嘲笑因此问题。

这在 Mockito 的 documentation 中也有提及,

Mockito.spy() 是创建部分模拟的推荐方法。原因是它保证对正确构造的对象调用真正的方法,因为您负责构造传递给 spy() 方法的对象。

doCallRealMethod() 是在一个模拟上调用的,但不能保证该对象会按照应有的方式创建。

所以要回答您的问题,是的,这就是您创建模拟的方式,但doCallRealMethod 始终是一场赌博,无论龙目岛如何。

如果你真的想调用实际的方法,你可以使用spy

  @Test
  void c() {
    Problem spyProblem = Mockito.spy(new Problem());
    assertEquals("c", spyProblem.c());
    verify(spyProblem, Mockito.times(1)).c();
  }

【讨论】:

  • 恕我直言,这是 Lombok 的问题。我在问题中概述的逻辑是准确且正确的。实际上,如果您查看我的“hack”评论,您会像我一样意识到ReflectionTestUtils.setField( subject, "$lock", new Object() ); 将解决问题,但这不能依赖。不过还是谢谢你的建议。此外,间谍不是我想要的。我故意将问题简化为可行的,所以让我们坚持使用 Mock,好心。
  • Lombok 有什么问题?看看我的测试用例,我在测试中放入的方法与 Lombok 无关,它仍然因 NPE 而失败,任何需要初始化的成员变量都不在 doCallRealMethod 中。您需要使用 ReflectionTestUtils 进行初始化。另外你为什么要使用mock并做一个doCallRealMethod,似乎很不寻常。
  • 因为 Lombok 使用了一个我们无法控制的隐藏字段,所以这是一个 Lombok 问题。看起来很简单。 Spy 创建一个实际的对象实例。模拟没有。这应该足够了。如果你不能按原样解决这个测试情况,那也没关系。也许没有其他解决方案来初始化 $lock 字段并希望多年来不会改变。这个问题是关于 Mock 的。再次感谢。
  • 好的,假设类问题在图书馆中。你仍然无法控制它的私人成员。我说这个问题不是 Lombok 特有的,对于类的任何 private 字段都是一样的。
  • 很抱歉您没有收到它。这不是关于宇宙的问题。您的解决方案不能解决 Lombok 模拟的问题。我已经花时间浏览 Lombok 源代码甚至配置,与您的回答不同,我已经找到了一个适用于 Mock 的解决方案。我会更清楚:我不想要间谍。我不能使用间谍。我不希望调用所有字段集或构造函数。我想要的只是@Synchronized,它是隐藏锁。由于我已经努力过源头,我现在知道要注入什么字段,现在知道了,关于龙目岛的这个问题就解决了。
【解决方案2】:

核心问题是您将调用真实方法与模拟而不是间谍结合起来。这是危险的一般,因为它是否适用于任何事情很大程度上取决于相关方法的内部实现。

Lombok 之所以重要,是因为它通过在编译期间更改内部实现来工作,这种方式恰好需要正确的对象初始化才能在原始方法不工作的情况下工作。

如果你要配置一个模拟来调用真实的方法,你应该使用一个间谍来代替。

【讨论】:

  • 感谢您的评论。我同意,只有当开发人员不知道模拟和间谍之间的区别时,这才是危险的。请在其他答案中查看上述 cmets。间谍不是灵丹妙药。这个问题应该没关系,但我不希望实例化对象。您必须考虑在实例化期间可能会发生哪些繁重的操作。所以这个问题是关于 Mock、Lombok 以及它创建的隐藏锁字段。不过还是谢谢你的建议。
  • @Drakes 如果繁重的操作作为对象实例化的一部分发生,那是另一个独立的问题。您有这个问题的事实是更喜欢避免间谍的原因,但这并不会改变doCallRealMethod throwing 的适当解决方案。我建议a)更改类以在构造函数之外执行昂贵的操作(无论如何我都会推荐),b)然后使用间谍。
  • 谢谢,道格拉斯。一个词:SpringBoot。这个问题已经让我满意地解决了。尽管如此,还是感谢您的反馈。
  • @Drakes SpringBoot 对这个问题有何贡献?您正在模拟一个您自己定义的类,而不是来自框架或库的类。
  • 这是一个最小的可行问题。我想我们已经完成了这个问题。
【解决方案3】:

概要

Project Lombok 在隐藏底层和自动生成的私有锁的方法上有 @Synchronized 注释,而 synchronized 锁定 this

当使用 Mockito 模拟时(不是间谍,因为在某些情况下我们不希望实例化完整的对象),字段不会被初始化。这也意味着自动生成的“锁定”字段为空,这会导致 NPE。

解决方案 1 - 现场注入

查看 Lombok source code,我们看到 Lombok 使用以下锁名称:

private static final String INSTANCE_LOCK_NAME = "$lock";
private static final String STATIC_LOCK_NAME = "$LOCK";

除非 Lombok 将来突然改变这一点,这意味着我们可以进行字段注入,即使感觉像是“黑客”:

@BeforeEach
void setup()
{
    initMocks(this);
    ...
    ReflectionTestUtils.setField( subject, "$lock", new Object[0] );
}

解决方案 2 - 声明一个锁,然后进行字段注入

问题是关于@Synchronized,而不是@Synchronized("someLockName"),但是如果你可以明确声明锁名,那么你可以对锁字段名有信心地使用解决方案一。

【讨论】:

    猜你喜欢
    • 2023-03-06
    • 1970-01-01
    • 1970-01-01
    • 2021-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多