英文:
Lombok @Synchronized with Mockito throws NPE
问题
给定` synchronized` 和 Lombok 的 `@Synchronized`,后者在模拟测试方法时会导致 `NullPointerException`。给定以下代码:
```java
public class Problem
{
public Problem()
{
// 昂贵的初始化,
// 所以使用 Mock,而不是 Spy
}
public synchronized String a()
{
return "a";
}
@Synchronized // <-- 在这里导致测试期间的 NPE
public String b()
{
return "b";
}
}
以及 Jupiter 测试类:
class ProblemTest
{
@Mock
private Problem subject;
@BeforeEach
void setup()
{
initMocks(this);
// 有更多的模拟。请不要让这个示例的简单性使你困惑。
doCallRealMethod().when(subject).a();
doCallRealMethod().when(subject).b();
// 这是一个 hack,但有效。我们可以依赖这个吗?
// ReflectionTestUtils.setField(subject, "$lock", new Object[0]);
}
@Test
void a()
{
// 成功
assertEquals("a", subject.a());
}
@Test
void b()
{
// 测试期间的 NullPointerException
assertEquals("b", subject.b());
}
}
Lombok 添加了类似以下内容:
private final Object $lock = new Object[0]; // 我们不能依赖这个名称
// ...
public String b()
{
synchronized($lock)
{
return "b";
}
}
如何模拟使用 Lombok 的 default @Synchronized
注解修饰的方法?
以下是堆栈跟踪,尽管不是很有用。我怀疑 Lombok 添加了一个类似我上面示例中的字段,当然这不会被注入到模拟中,所以就有了 NPE。
java.lang.NullPointerException
at com.ericdraken.Problem.b(Problem.java:16) // <-- @Synchronized 关键字
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)
英文:
Given synchronized
and Lombok's @Synchronized
, the latter causes a NullPointerException
when mocking a method under test. Given
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";
}
}
and the Jupiter test class
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 adds something like the following:
private final Object $lock = new Object[0]; // We can't rely on this name
...
public String b()
{
synchronized($lock)
{
return "b";
}
}
How to mock a method that is decorated with Lombok's default @Synchronized
annotation?
Here is the stack trace, though it is unhelpful. I suspect Lombok adds a field as in my example above, and of course that is not injected into the mock, so voilà, 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)
答案1
得分: 2
这不是 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
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 的文档中也有提到,
Mockito.spy()
是创建部分模拟的推荐方法。原因是,它可以保证对正确构造的对象调用真实方法,因为你需要负责构造传递给spy()
方法的对象。
对于调用 doCallRealMethod()
的模拟对象,不能保证对象的创建方式是正确的。
所以,回答你的问题,是的,这是创建模拟对象的方式,但是 doCallRealMethod
总是一种不确定的方法,与是否使用 Lombok 无关。
如果你真的想调用实际方法,你可以使用 spy
。
@Test
void c() {
Problem spyProblem = Mockito.spy(new Problem());
assertEquals("c", spyProblem.c());
verify(spyProblem, Mockito.times(1)).c();
}
英文:
This isn't an issue with Lombok, the following also fails.
@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");
}
}
To be precise, you are not really mocking Problem
, you are partially mocking via doCallRealMethod
hence the issue.
This is also called out in Mockito's documentation,
>Mockito.spy() is a recommended way of creating partial mocks. The reason is it guarantees real methods are called against correctly constructed object because you're responsible for constructing the object passed to spy() method.
doCallRealMethod()
is called on a mock which is not guaranteed to have the object created the way it's supposed to be.
So to answer your question, yes that's the way you create a mock, but doCallRealMethod
is always a gamble irrespective of Lombok.
You can use spy
if you really want to call the actual method.
@Test
void c() {
Problem spyProblem = Mockito.spy(new Problem());
assertEquals("c", spyProblem.c());
verify(spyProblem, Mockito.times(1)).c();
}
答案2
得分: 0
概要
Project Lombok在方法上使用@Synchronized
注解来隐藏基础的自动生成的私有锁,而synchronized
则锁定this
。
在使用Mockito模拟对象时(不是spy,因为在某些情况下,我们不希望完整实例化对象),字段没有被初始化。这意味着自动生成的“lock”字段也是null,从而导致了空指针异常。
解决方案1 - 字段注入
从Lombok的源代码中可以看出,Lombok使用以下锁名称:
private static final String INSTANCE_LOCK_NAME = "$lock";
private static final String STATIC_LOCK_NAME = "$LOCK";
除非Lombok在未来突然更改了这一点,这意味着即使感觉有些像“hack”,我们仍然可以进行字段注入:
@BeforeEach
void setup()
{
initMocks(this);
...
ReflectionTestUtils.setField(subject, "$lock", new Object[0]);
}
解决方案2 - 声明一个锁,然后进行字段注入
这个问题是关于@Synchronized
,而不是@Synchronized("someLockName")
,但如果你可以显式地声明锁的名称,那么你可以放心地使用解决方案一,并确信锁字段的名称。
英文:
Synopsis
Project Lombok has the @Synchronized
annotation on methods to hide the underlying and auto-generated private lock(s), whereas synchronized
locks on this
.
When using a Mockito mock (not a spy, because there are situations when we don't want a full object instantiated), fields are not initialized. That means as well the auto-generated "lock" field is null which causes the NPE.
Solution 1 - Field injection
Looking at Lombok source code, we see that Lombok uses the following lock names:
private static final String INSTANCE_LOCK_NAME = "$lock";
private static final String STATIC_LOCK_NAME = "$LOCK";
Unless Lombok suddenly changes this in the future, this means we can do field injection even if it feels like a "hack":
@BeforeEach
void setup()
{
initMocks(this);
...
ReflectionTestUtils.setField( subject, "$lock", new Object[0] );
}
Solution 2 - Declare a lock, then field injection
The question asks about @Synchronized
, not @Synchronized("someLockName")
, but if you can explicitly declare the lock name, then you can use solution one with confidence about the lock field name.
答案3
得分: 0
核心问题在于你将调用真实方法与模拟方法结合在一起,而不是使用间谍(spy)。这在一般情况下是危险的,因为它是否适用于任何情况很大程度上取决于所讨论的方法的内部实现。
Lombok之所以重要,是因为它通过在编译期间修改内部实现的方式来工作,而这种方式恰好需要适当的对象初始化,以使原始方法不起作用。
如果你要配置一个模拟方法来调用真实方法,你应该考虑使用间谍(spy)。
英文:
The core problem is that you are combining calling the real method with a mock rather than a spy. This is dangerous in general, as whether it works for anything depends very much on the internal implementation of the method in question.
Lombok only matters because it works by altering that internal implementation during compilation, in a way that happens to require proper object initialization to work where the original method does not.
If you're going to configure a mock to call the real method, you should probably use a spy instead.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论