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

如何简洁验证可构建对象的方法调用且不暴露类成员?

如何简洁验证可构建对象的方法调用且不暴露类成员?

有时候我们需要验证被测系统(SUT)类的某个方法是否被调用,但如果这个类是采用Joshua Bloch在《Effective Java》中提到的构建器模式实现的,事情就会变得有点棘手。

我最近做了个简单测试:想验证自己扩展的JDialog在点击OK按钮后确实会被dispose掉。但如果不用那种很繁琐的Mock初始化方式(见下文代码块),测试就会失败。而且我还得把构造函数改成public——这其实是我不想做的,毕竟Mockito默认没法调用私有构造函数。

我想知道怎么能简洁地完成这个测试,还不用不必要地暴露类成员(比如构造函数)?另外有个关键点:测试过程中不能实际显示任何GUI元素,这样就算在没有图形环境的情况下测试也能通过。

无繁琐初始化的写法(测试失败)

import org.junit.jupiter.api.Test;

import javax.swing.JButton;

import static org.mockito.Mockito.*;

class MyDialogTest {
    @Test
    void okButtonClickShouldDisposeDialog() {
        // 用构建器创建实例,但Mockito无法监控dispose方法调用
        MyDialog dialog = MyDialog.builder().build();
        JButton okButton = dialog.getOkButton(); // 假设存在这个获取按钮的方法

        okButton.doClick();

        verify(dialog).dispose(); // 测试失败,因为dialog不是Mock/Spy对象
    }
}

繁琐初始化的写法(测试通过但不够优雅)

import org.junit.jupiter.api.Test;

import javax.swing.JButton;

import static org.mockito.Mockito.*;

class MyDialogTest {
    @Test
    void okButtonClickShouldDisposeDialog() throws Exception {
        // 通过反射调用私有构造函数创建实例,再包装成Spy
        MyDialog dialog = spy(MyDialog.class.getDeclaredConstructor().newInstance());
        // 可能还要手动初始化按钮等组件,流程非常繁琐

        JButton okButton = dialog.getOkButton();
        okButton.doClick();

        verify(dialog).dispose(); // 测试通过,但代码冗余且暴露了构造细节
    }
}

推荐的简洁解决方案

这里有几个优雅的方案可以帮你解决问题,既不用破坏类的封装性,也能保证测试在无图形环境下正常运行:

1. 用Mockito Spy包装构建器创建的实例

Mockito的spy()方法可以直接包装真实实例,只要你的构建器能生成完整的对象,就可以把它转成Spy来监控方法调用。另外别忘了提前设置Headless模式,避免GUI真的显示出来:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;

import javax.swing.JButton;
import java.awt.GraphicsEnvironment;

import static org.mockito.Mockito.*;

class MyDialogTest {
    // 提前启用Headless模式,确保无图形环境也能运行
    @BeforeAll
    static void setupHeadlessMode() {
        GraphicsEnvironment.getLocalGraphicsEnvironment().setHeadless(true);
    }

    @Test
    void okButtonClickShouldDisposeDialog() {
        // 用构建器创建真实实例,再包装成Spy
        MyDialog dialog = spy(MyDialog.builder().build());
        
        // 如果你不想让真实的dispose()执行(比如避免GUI操作),可以打桩跳过
        doNothing().when(dialog).dispose();

        JButton okButton = dialog.getOkButton();
        okButton.doClick();

        verify(dialog).dispose(); // 现在可以成功验证方法调用了!
    }
}

这个方案的优势是完全沿用你原有的构建器流程,不用修改类的内部结构,只是给实例加了一层监控。Headless模式的设置也能满足“不显示GUI”的要求。

2. 给构建器添加监听器注入点

如果你的MyDialog中OK按钮的点击逻辑是硬编码的,可以把监听器改成可注入的方式,这样测试时可以替换成自定义监听器,直接验证逻辑,甚至不用创建完整的Dialog实例:

// 修改后的MyDialog类,支持注入按钮监听器
class MyDialog extends JDialog {
    private final JButton okButton;

    // 私有构造函数,仅供构建器使用
    private MyDialog(Builder builder) {
        this.okButton = new JButton("OK");
        // 优先使用注入的监听器,否则用默认的dispose逻辑
        okButton.addActionListener(builder.okActionListener != null 
            ? builder.okActionListener 
            : e -> this.dispose());
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private ActionListener okActionListener;

        // 添加监听器注入方法
        public Builder okActionListener(ActionListener listener) {
            this.okActionListener = listener;
            return this;
        }

        public MyDialog build() {
            return new MyDialog(this);
        }
    }

    // 供测试获取按钮的方法(如果需要的话)
    public JButton getOkButton() {
        return okButton;
    }
}

对应的测试代码可以这么写:

import org.junit.jupiter.api.Test;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import static org.mockito.Mockito.*;

class MyDialogTest {
    @Test
    void okButtonClickShouldTriggerDispose() {
        // 创建Mock监听器
        ActionListener mockListener = mock(ActionListener.class);
        // 通过构建器注入监听器
        MyDialog dialog = MyDialog.builder()
            .okActionListener(mockListener)
            .build();

        JButton okButton = dialog.getOkButton();
        okButton.doClick();

        // 验证监听器被触发,间接证明dispose逻辑会执行
        verify(mockListener).actionPerformed(any(ActionEvent.class));

        // 或者结合Spy直接验证dispose:
        MyDialog spyDialog = spy(MyDialog.builder().build());
        spyDialog.getOkButton().doClick();
        verify(spyDialog).dispose();
    }
}

这种方式遵循依赖注入的思想,让测试更灵活,同时保持类的封装性,私有构造函数依然可以保留。

3. 用PowerMock绕过私有构造(不推荐)

如果上面的方案都不适用(比如构建器逻辑极度复杂),可以用PowerMock来绕过私有构造函数的限制,但我不太推荐这个方案——因为PowerMock会修改类加载器,可能带来一些潜在的兼容性问题。如果必须用,代码大概是这样:

import org.junit.jupiter.api.Test;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import javax.swing.JButton;

import static org.mockito.Mockito.*;

@RunWith(PowerMockRunner.class)
@PrepareForTest(MyDialog.class)
class MyDialogTest {
    @Test
    void okButtonClickShouldDisposeDialog() throws Exception {
        // 用PowerMock创建Spy,无需调用构造函数
        MyDialog dialog = PowerMockito.spy(MyDialog.class);
        // 打桩构建器方法,返回Spy实例
        PowerMockito.when(MyDialog.builder().build()).thenReturn(dialog);

        JButton okButton = mock(JButton.class);
        // 打桩获取按钮的方法
        when(dialog.getOkButton()).thenReturn(okButton);

        okButton.doClick();

        verify(dialog).dispose();
    }
}

还是优先推荐前两种方案,尤其是第一种,简单直接,对原有代码的侵入性最小。

备注:内容来源于stack exchange,提问作者Cagepi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 14:23:06