问题描述
给出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() );
}
}
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)
解决方法
这不是Lombok的问题,以下操作也会失败。
BehaviorSubject
确切地说,您并不是在嘲笑@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
部分嘲笑,因此是问题所在。
这在Mockito的documentation中也被提及,
Mockito.spy()是创建部分模拟的推荐方法。原因是它确保针对正确构造的对象调用真实方法,因为您负责构造传递给spy()方法的对象。
doCallRealMethod
在模拟中调用,不能保证以预期的方式创建对象。
所以要回答您的问题,是的,这就是创建模拟的方式,但是doCallRealMethod()
始终是赌博,与Lombok无关。
如果您确实要调用实际方法,则可以使用doCallRealMethod
。
spy
,
简介
Lombok项目在方法上具有@Synchronized
注释,用于隐藏基础和自动生成的私人锁,而synchronized
则对this
进行锁。
在使用Mockito模拟(不是间谍,因为在某些情况下我们不希望实例化完整的对象)时,不会初始化字段。这也意味着自动生成的“锁定”字段为空,这会导致NPE。
解决方案1-场注入
看看龙目岛source code,我们看到龙目岛使用以下锁名:
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")
,但是如果您可以显式声明锁名,则可以对锁字段名放心使用解决方案一。
核心问题是您将调用真实方法与模拟而不是间谍结合在一起。 这通常很危险,因为它是否适用于任何东西在很大程度上取决于所讨论方法的内部实现。
Lombok之所以重要,是因为它通过在编译过程中更改内部实现来起作用,而这种方式恰好需要正确的对象初始化才能在原始方法不起作用的情况下起作用。
如果您要配置一个模拟来调用真实方法,则应该改用间谍。