如何用Mockito Stub内部调用的包级保护网络方法?
我有一个AddressSearcher类,接收多个对象作为参数,可以创建真实依赖和真实被测对象。类里有个包级保护方法getDomainFromIp,负责执行网络调用,会被checkAddress方法内部调用。我想用Mockito只Stub这个方法,但常规的Mockito Spy不生效:直接调用Stub后的getDomainFromIp符合预期,但通过checkAddress调用时还是执行真实方法。我能用JMockit轻松实现,但不能修改代码把这个网络方法改成可注入的依赖。
被测类代码
public class AddressSearcher { private final AddressSearcherSettings settings; private final AddressList addressList; private final OtherDependency otherDependency; public AddressSearcher( final AddressSearcherSettings settings, final AddressList addressList, final OtherDependency otherDependency ) { this.settings = settings; this.addressList = addressList; this.otherDependency = otherDependency; } public Domain checkAddress(String address) { if (addressList.contains(address)) { return addressList.get(address); } Domain domain = this.getDomainFromIp(address); if (domain.isNotEmpty()) { return domain; } return EmptyDomain(); } Domain getDomainFromIp(String address) { // 执行网络调用,根据IP查询域名 } }
测试代码(常规Spy失效)
@Test public void checkAddressTest() { // 初始化真实依赖 AddressSearcherSettings realSettings = ...; AddressList realAddressList = ...; OtherDependency realOtherDependency = ...; AddressSearcher realAddressSearcher = new AddressSearcher(realSettings, realAddressList, realOtherDependency); AddressSearcher spiedAddressSearcher = Mockito.spy(realAddressSearcher); // 直接调用Stub方法符合预期 Mockito.doReturn(new Domain("Google")).when(this.spiedAddressSearcher).getDomainFromIp("8.8.8.8"); assertEquals(new Domain("Google"), this.spiedAddressSearcher.getDomainFromIp("8.8.8.8")); // 但通过checkAddress调用时,仍执行真实方法,返回EmptyDomain assertEquals(new Domain("Google"), this.spiedAddressSearcher.checkAddress("8.8.8.8")); }
常规Mockito Spy失效的核心原因是:Mockito Spy基于动态代理实现,当你调用spiedAddressSearcher.checkAddress时,代理会转发调用到原始AddressSearcher实例的checkAddress方法。而原始实例内部的this.getDomainFromIp调用的是自身的方法,不会经过代理对象,因此Stub逻辑完全不会被触发。
方案一:使用Mockito Mock对象+反射注入真实依赖
创建AddressSearcher的Mock对象,指定checkAddress调用真实逻辑,再通过反射(或Mockito的Whitebox工具)将真实依赖注入到Mock对象的私有字段中,最后StubgetDomainFromIp方法。此时checkAddress内部的this指向Mock对象,调用getDomainFromIp会触发Stub逻辑。
@Test public void checkAddressTest() throws NoSuchFieldException, IllegalAccessException { // 初始化真实依赖 AddressSearcherSettings realSettings = new AddressSearcherSettings(); AddressList realAddressList = new AddressList(); OtherDependency realOtherDependency = new OtherDependency(); // 创建Mock对象 AddressSearcher mockSearcher = Mockito.mock(AddressSearcher.class); // 让checkAddress调用真实业务逻辑 Mockito.when(mockSearcher.checkAddress(Mockito.anyString())).thenCallRealMethod(); // 反射注入真实依赖到Mock对象的私有字段 Field settingsField = AddressSearcher.class.getDeclaredField("settings"); settingsField.setAccessible(true); settingsField.set(mockSearcher, realSettings); Field addressListField = AddressSearcher.class.getDeclaredField("addressList"); addressListField.setAccessible(true); addressListField.set(mockSearcher, realAddressList); Field otherDependencyField = AddressSearcher.class.getDeclaredField("otherDependency"); otherDependencyField.setAccessible(true); otherDependencyField.set(mockSearcher, realOtherDependency); // Stub getDomainFromIp方法 Mockito.doReturn(new Domain("Google")).when(mockSearcher).getDomainFromIp("8.8.8.8"); // 执行测试,此时会返回Stub的Domain assertEquals(new Domain("Google"), mockSearcher.checkAddress("8.8.8.8")); }
方案二:使用PowerMockito拦截内部方法调用
PowerMockito通过字节码修改技术,可以直接拦截原始对象的内部方法调用,无需依赖代理对象,完美解决常规Spy的局限性。
测试代码
@RunWith(PowerMockRunner.class) @PrepareForTest(AddressSearcher.class) // 指定需要修改字节码的类 public class AddressSearcherTest { @Test public void checkAddressTest() { // 初始化真实依赖与真实实例 AddressSearcherSettings realSettings = new AddressSearcherSettings(); AddressList realAddressList = new AddressList(); OtherDependency realOtherDependency = new OtherDependency(); AddressSearcher realSearcher = new AddressSearcher(realSettings, realAddressList, realOtherDependency); // 创建PowerMock Spy PowerMockito.spy(realSearcher); // Stub内部调用的getDomainFromIp方法 PowerMockito.doReturn(new Domain("Google")).when(realSearcher, "getDomainFromIp", "8.8.8.8"); // 执行测试,此时checkAddress内部的getDomainFromIp会触发Stub逻辑 assertEquals(new Domain("Google"), realSearcher.checkAddress("8.8.8.8")); } }
内容的提问来源于stack exchange,提问作者candelphi

