You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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父类构造:

  1. 添加依赖到gradle:
testImplementation 'org.robolectric:robolectric:4.12.1'
  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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 18:42:03