Mockito:如何处理从已有对象生成新对象的方法测试问题?
问题原因
Android本地单元测试(JVM环境)默认使用的android.jar中,SDK类(如android.location.Location)的方法大多是空Stub实现,调用未被Stub的方法就会抛出类似Method setTime in android.location.Location not mocked的异常。你的MyLocation继承自Location,构造新实例时触发了Location内部未Stub的方法,因此报错。这不是反模式,是Android单元测试的默认限制导致的常见问题。
解决方案
1. 抽象依赖,解耦Android SDK类
把Location的核心属性(纬度、经度等)抽象成独立接口,让MyLocation依赖接口而非直接继承Location。这样测试时完全避开Android SDK类,只验证计算逻辑:
// 抽象位置数据接口 public interface LocationData { double getLatitude(); double getLongitude(); } // 自定义位置类,实现接口 public class MyLocation implements LocationData { private final double latitude; private final double longitude; // 从Android Location转换的构造器 public MyLocation(Location location) { this.latitude = location.getLatitude(); this.longitude = location.getLongitude(); } // 用于计算的构造器 public MyLocation(double latitude, double longitude) { this.latitude = latitude; this.longitude = longitude; } public MyLocation move(double speed, double dir, int seconds) { double deltaLat = Math.cos(dir) * speed * seconds; double deltaLng = Math.sin(dir) * speed * seconds; return new MyLocation(latitude + deltaLat, longitude + deltaLng); } @Override public double getLatitude() { return latitude; } @Override public double getLongitude() { return longitude; } }
测试时直接构造MyLocation实例,验证计算结果即可,完全不会触发Android SDK的方法:
@Test public void testMoveCalculation() { MyLocation initialLoc = new MyLocation(0.0, 0.0); MyLocation newLoc = initialLoc.move(10, Math.PI/2, 1); assertEquals(0.0, newLoc.getLatitude(), 0.001); assertEquals(10.0, newLoc.getLongitude(), 0.001); }
2. 使用Robolectric提供真实Android类实现
Robolectric可以在JVM测试中提供Android SDK类的真实实现,无需Mock就能让Location的方法正常执行。在测试类上添加@RunWith(RobolectricTestRunner.class),直接构造MyLocation实例即可:
@RunWith(RobolectricTestRunner.class) public class MyLocationTest { @Test public void testMove() { MyLocation initialLoc = new MyLocation("test"); // 使用Location的合法构造器 initialLoc.setLatitude(0.0); initialLoc.setLongitude(0.0); MyLocation newLoc = initialLoc.move(10, Math.PI/2, 1); assertEquals(0.0, newLoc.getLatitude(), 0.001); assertEquals(10.0, newLoc.getLongitude(), 0.001); } }
3. Mock Location的必要方法(临时方案)
如果不想修改原有代码,可以在测试中用Mockito StubLocation的所有必要方法,避免报错。但这种方法需要Stub多个方法,比较繁琐:
@RunWith(MockitoJUnitRunner.class) public class MyLocationTest { @Mock Location mockLocation; @Test public void testMove() { // Stub Location的核心方法 when(mockLocation.getLatitude()).thenReturn(0.0); when(mockLocation.getLongitude()).thenReturn(0.0); // Stub构造时会调用的setTime方法 doNothing().when(mockLocation).setTime(anyLong()); // 若还有其他未Stub方法报错,需继续添加Stub MyLocation myLoc = new MyLocation(mockLocation); MyLocation newLoc = myLoc.move(10, Math.PI/2, 1); assertEquals(0.0, newLoc.getLatitude(), 0.001); assertEquals(10.0, newLoc.getLongitude(), 0.001); } }
总结
优先选择抽象依赖的方案,它能彻底解耦Android SDK,让测试更纯净、易维护;如果需要保留与Location的继承关系,推荐用Robolectric;Mock方法仅作为临时应急方案。
内容的提问来源于stack exchange,提问作者user998303

