如何用Mockito和JUnit覆盖构造方法父类逻辑及init方法?
问题描述
需要为SimulationDialog类的init()方法实现测试覆盖,但调试时super(context)行抛出NullPointerException,提示父类方法存在异常,尝试多种方法仍未解决。
被测代码(SimulationDialog.java)
public class SimulationDialog extends DialogView { private static final double BUTTON_STEP = 1; private static final double STEPPER_MIN = 0; private static final double STEPPER_MAX_CNH = 7500; private static final double STEPPER_MAX_AG_LEADER = 426000; private static final Logger logger = LoggerFactory.getLogger(SimulationDialog.class); private float currentStepperValue; private boolean isSwitchOn; /** * 接收原始力控件的UI变更 */ public interface SimulationDialogListener { /** * 模拟器开关状态变更时调用 * * @param started 开关为ON时返回true,否则返回false */ void onSimulationToggle(boolean started); /** * 用户更新模拟原始力值时调用 * * @param rawForce 用户在{@link StepperView}中输入的原始力值 */ void onSimulatedRawForceUpdated(double rawForce); } private SimulationDialogListener listener; private OnOffSwitch onOffSwitch; private StepperView stepperView; private TextView simulationNote; private FrameLayout stepperBg; public SimulationDialog(Context context) { super(context); init(); } @VisibleForTesting protected void init() { DialogView dialogView = setBodyView(R.layout.simulation_dialog); this.onOffSwitch = (OnOffSwitch) dialogView.findViewById(R.id.toggleSimulation); this.stepperView = (StepperView) dialogView.findViewById(R.id.fakeRawForce); this.simulationNote = (TextView) dialogView.findViewById(R.id.simulationNote); this.stepperBg = (FrameLayout) dialogView.findViewById(R.id.stepper_bg); this.stepperView.setOnKeyboardValueUpdateListener(new StepperView.OnKeyboardValueUpdateListener() { @Override public boolean onKeyboardValueInserted(float keyValue) { currentStepperValue = keyValue; listener.onSimulatedRawForceUpdated(currentStepperValue); return false; } }); // 省略剩余代码...... } }
当前测试代码(SimulationDialogTest.java)
package com.cnh.pf.android.libymui.dialog; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; import com.cnh.android.dialog.DialogView; import com.cnh.android.widget.control.OnOffSwitch; import com.cnh.android.widget.control.StepperView; import com.cnh.pf.android.libymui.R; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.spy; /** * Created by Tiago Santos on 04/03/2024 */ @RunWith(PowerMockRunner.class) @PowerMockIgnore("javax.management.*") @PrepareForTest({LayoutInflater.class}) public class SimulationDialogTest { @Mock private DialogView mockDialogView; @Mock private OnOffSwitch mockOnOffSwitch; @Mock private TextView mockSimulationNote; @Mock private FrameLayout mockStepperBg; @Mock private Context mockContext; @Mock private DialogView dialogView; @Mock private StepperView mockStepperView; @Mock private OnOffSwitch onOffSwitch; @Mock private SimulationDialog.SimulationDialogListener listenerMock; @Mock private View dialogViewMock; @Mock private LayoutInflater mockLayoutInflater; private SimulationDialog simulationDialog; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); simulationDialog = spy(new SimulationDialog(mockContext)); mockStatic(LayoutInflater.class); when(LayoutInflater.from(mockContext)).thenReturn(mockLayoutInflater); when(mockLayoutInflater.inflate(eq(R.layout.simulation_dialog), any(FrameLayout.class), anyBoolean())).thenReturn(dialogViewMock); simulationDialog.init(); } @Test public void testInit() { } }
解决方案
1. 解决父类构造NPE问题
DialogView构造依赖真实Android环境组件,直接传入Mock的Context会导致初始化失败,可通过以下两种方式解决:
方案A:PowerMock局部Mock父类逻辑
修改setUp方法,跳过父类真实初始化,Mock关键方法返回值:
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); // Mock子类,调用真实构造但跳过父类的异常逻辑 simulationDialog = mock(SimulationDialog.class, withSettings() .useConstructor(mockContext) .defaultAnswer(CALLS_REAL_METHODS)); // 模拟setBodyView返回Mock的DialogView doReturn(dialogView).when(simulationDialog).setBodyView(eq(R.layout.simulation_dialog)); // 模拟findViewById返回对应Mock控件 when(dialogView.findViewById(eq(R.id.toggleSimulation))).thenReturn(mockOnOffSwitch); when(dialogView.findViewById(eq(R.id.fakeRawForce))).thenReturn(mockStepperView); when(dialogView.findViewById(eq(R.id.simulationNote))).thenReturn(mockSimulationNote); when(dialogView.findViewById(eq(R.id.stepper_bg))).thenReturn(mockStepperBg); // 反射设置listener,避免回调NPE Field listenerField = SimulationDialog.class.getDeclaredField("listener"); listenerField.setAccessible(true); listenerField.set(simulationDialog, listenerMock); // 调用真实init方法 simulationDialog.init(); }
方案B:使用Robolectric模拟Android环境
Robolectric提供完整的Android模拟环境,无需Mock父类构造:
- 添加依赖到gradle:
testImplementation 'org.robolectric:robolectric:4.12.1'
- 修改测试类:
@RunWith(RobolectricTestRunner.class) public class SimulationDialogTest { @Mock private SimulationDialog.SimulationDialogListener listenerMock; @Mock private StepperView mockStepperView; private SimulationDialog simulationDialog; @Before public void setUp() throws NoSuchFieldException, IllegalAccessException { MockitoAnnotations.initMocks(this); // 获取真实模拟上下文 Context context = ApplicationProvider.getApplicationContext(); simulationDialog = new SimulationDialog(context); // 反射替换内部stepperView为Mock对象 Field stepperField = SimulationDialog.class.getDeclaredField("stepperView"); stepperField.setAccessible(true); stepperField.set(simulationDialog, mockStepperView); // 反射设置listener Field listenerField = SimulationDialog.class.getDeclaredField("listener"); listenerField.setAccessible(true); listenerField.set(simulationDialog, listenerMock); } @Test public void testInit() throws NoSuchFieldException, IllegalAccessException { // 验证控件初始化完成 Field onOffSwitchField = SimulationDialog.class.getDeclaredField("onOffSwitch"); onOffSwitchField.setAccessible(true); Assert.assertNotNull(onOffSwitchField.get(simulationDialog)); // 验证StepperView监听器设置 ArgumentCaptor<StepperView.OnKeyboardValueUpdateListener> captor = ArgumentCaptor.forClass(StepperView.OnKeyboardValueUpdateListener.class); verify(mockStepperView).setOnKeyboardValueUpdateListener(captor.capture()); // 触发监听器回调,验证逻辑正确性 captor.getValue().onKeyboardValueInserted(100f); verify(listenerMock).onSimulatedRawForceUpdated(100d); // 验证currentStepperValue更新 Field valueField = SimulationDialog.class.getDeclaredField("currentStepperValue"); valueField.setAccessible(true); Assert.assertEquals(100f, valueField.getFloat(simulationDialog), 0.01f); } }
2. 完善init方法测试覆盖
测试需覆盖以下核心点:
- 验证
setBodyView被调用,传入正确布局ID - 验证所有控件通过
findViewById正确初始化 - 验证
StepperView监听器设置成功,且回调逻辑正常(更新值+通知listener)
内容的提问来源于stack exchange,提问作者Tiago Ribeiro Santos
相关产品推荐
相关产品推荐

