写单元测试时碰到一个问题,用@Mock mock一个类的成员变量,然后用@InjectMocks把mock的对象注入到要测试的类时,报空指针异常,初步定位是mock对象注入失败。
google、ds搜索无果,类名前添加@ExtendWith(MockitoExtension.class)之类的低级错误,仔细检查后都没有,于是开始debug Mockito源码。中间过程耗费了一些时间,分享一些经验给大家。
假设有一个典型的Spring Boot应用。Service层简化如下:
@Service
public class TestService {
@Resource
private TestDao1 dao1;
@Resource
private TestDao2 dao2;
public TestService(TestDao1 dao1) {
this.dao1 = dao1;
}
public String getResult1() {
return dao1.getResult();
}
public String getResult2() {
return dao2.getResult();
}
}
DAO层:
@Mapper
public interface TestDao1 {
String getResult();
}
@Mapper
public interface TestDao2 {
String getResult();
}
对Service层做单元测试,单元测试代码如下:
@ExtendWith(MockitoExtension.class)
class TestServiceTest {
@InjectMocks
private TestService testService;
@Mock
private TestDao1 testDao1;
@Mock
private TestDao2 testDao2;
@Test
void getResult1() {
Mockito.when(testDao1.getResult()).thenReturn("from dao1");
assertEquals("from dao1", testService.getResult1());
}
@Test
void getResult2() {
Mockito.when(testDao2.getResult()).thenReturn("from dao2");
assertEquals("from dao2", testService.getResult2());
}
}
以上单元测试,期望全部成功。实际getResult1成功,getResult2失败。

why?!
testDao1和testDao2明明一样的mock方式,为什么一个注入成功了,一个失败了?!
请教各类AI和搜索引擎,都没找到解决方法,于是开始debug Mockito源码尝试找答案。
调试发现,testDao2是mock成功的。

那原因就是mock后没注入成功,定位方向,要转变为为什么没注入成功。
我们知道,mock时类名上要标注junit扩展的注解,那入口一定在MockitoExtension里。

点进去后,是我们单元测试熟悉的beforeEach,根据方法名推断,startMocking应该就是实际mock和注入的操作。

继续调试下去,到DefaultInjectionEngine,可以看到Mockito是先后依次进行了构造方法注入和字段注入

在构造方法注入里,有个寻找最大构造方法的调用。

计算方法是比较构造参数的长度。如果两个构造方法的参数长度相同,就比较二者可mock的参数长度。

如果要注入mock对象的类,只有默认构造方法,即构造方法参数长度为0,则抛异常,构造方法注入失败,使用第二种方式:字段注入。

选到最大构造方法后,则直接调用该构造函数,把所有的mock参数传入该构造方法,创建实例,并返回注入成功,跳过后续的字段注入。

到这里,一切都非常明朗了。
Mockito“选择最大构造函数”的策略,选错了构造方法,导致注入失败。
解决方法,至少有如下几种:
方法一:添加全量构造参数。
如下,修改TestService,增加如下构造方法。
public TestService(TestDao1 dao1, TestDao2 dao2) {
this.dao1 = dao1;
this.dao2 = dao2;
}
或者使用Lombok,给TestService类增加注解@AllArgsConstructor。

方法二:删除原来的构造方法,使用字段注入。
如下代码删除:
public TestService(TestDao1 dao1) {
this.dao1 = dao1;
}
方法三:使用反射,手动注入TestDao2。
在TestServiceTest种新增如下代码:
@BeforeEach
void setUp() throws Exception {
Field testDao2Field = testService.getClass().getDeclaredField("dao2");
testDao2Field.setAccessible(true);
testDao2Field.set(testService, testDao2);
testDao2Field.setAccessible(false);
}
最后,我认为Mockito这种根据参数列表长度选择构造方法的策略,非常简陋且ugly。应该按参数类型选择,如果没有合适的构造方法,应该使用字段注入,这样最稳妥。
Mockito @Mock注入失败解析
2089

被折叠的 条评论
为什么被折叠?



