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

如何Mock第三方Builder结果测试Java推送服务并提升可测试性

解决方法和可测试性优化方案

一、Mock Builder生成的Client(快速实现测试覆盖)

你当前的核心问题是Pusher的connect方法硬编码使用ClientBuilder创建Client,无法隔离真实推送服务器依赖。有两种快速实现测试的方式:

1. 用PowerMock静态Mock(不改现有代码)

如果暂时不想调整业务代码,用PowerMock直接Mock ClientBuilder的静态方法,绕开真实Client的创建逻辑:

@RunWith(PowerMockRunner.class)
@PrepareForTest({AndroidPusher.class, ClientBuilder.class})
public class AndroidPusherTest {
    @Mock
    private Client mockClient;

    @Test
    public void testSendNotification() throws Exception {
        // 让ClientBuilder.build()返回Mock的Client实例
        PowerMockito.mockStatic(ClientBuilder.class);
        when(ClientBuilder.create().setXXX(any()).build()).thenReturn(mockClient);

        AndroidPusher pusher = new AndroidPusher();
        pusher.connect();
        pusher.sendNotification("test-token", "test-content");

        // 验证Client的send方法是否被正确调用
        verify(mockClient).send(any(PushMessage.class));
    }
}

注意:这是临时救急方案,PowerMock会绕过常规类加载逻辑,长期使用会增加测试维护成本,能改代码的话优先选下面的方式。

2. 给Pusher注入ClientBuilder(小改代码提升可测试性)

不在Pusher内部硬编码ClientBuilder.create(),而是将ClientBuilder作为构造参数传入,同时保留默认构造方法适配Spring依赖注入:

public class AndroidPusher implements Pusher {
    private Client client;
    private final ClientBuilder clientBuilder;

    // 测试用构造方法,注入Mock的Builder
    public AndroidPusher(ClientBuilder clientBuilder) {
        this.clientBuilder = clientBuilder;
    }

    // Spring依赖注入用的默认构造方法
    public AndroidPusher() {
        this.clientBuilder = ClientBuilder.create();
    }

    @Override
    public void connect() {
        this.client = clientBuilder.setApiKey("xxx").build();
    }

    // ...sendNotification和disconnect方法
}

测试时可以轻松Mock Builder:

public class AndroidPusherTest {
    @Mock
    private ClientBuilder mockBuilder;
    @Mock
    private Client mockClient;

    @Test
    public void testSend() {
        when(mockBuilder.setApiKey(anyString())).thenReturn(mockBuilder);
        when(mockBuilder.build()).thenReturn(mockClient);

        AndroidPusher pusher = new AndroidPusher(mockBuilder);
        pusher.connect();
        pusher.sendNotification("token", "content");

        verify(mockClient).send(any());
    }
}

这种改动极小,完全不影响现有业务逻辑,同时大幅提升了可测试性。

二、优化类设计的可测试性(长期方案)

当前设计的核心问题是Pusher实现类耦合了Client的创建逻辑,导致无法隔离外部推送库依赖。可以从以下几点优化:

1. 拆分Client创建逻辑到独立Factory类

定义PushClientFactory接口,分别实现Android和iOS版本的工厂类,专门负责Client的创建:

public interface PushClientFactory {
    Client createClient();
}

public class AndroidClientFactory implements PushClientFactory {
    @Override
    public Client createClient() {
        return ClientBuilder.create().setApiKey("android-key").build();
    }
}

public class IosClientFactory implements PushClientFactory {
    @Override
    public Client createClient() {
        return IosClientBuilder.create().setCertPath("xxx").build();
    }
}

然后让Pusher依赖Factory,不再负责Client的创建:

public class AndroidPusher implements Pusher {
    private Client client;
    private final PushClientFactory clientFactory;

    @Autowired
    public AndroidPusher(PushClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }

    @Override
    public void connect() {
        this.client = clientFactory.createClient();
    }

    // ...其他方法
}

测试时直接Mock PushClientFactory返回假的Client,彻底与外部推送库解耦。

2. 调整Service中Pusher的复用逻辑

根据你补充的需求,Service周期性运行、首次创建Pusher并复用,每次发送都执行connect/send/disconnect。可以用ConcurrentHashMap缓存Pusher实例,通过Factory创建:

@Service
public class PushService {
    private final MessageDao messageDao;
    private final Map<String, Pusher> pusherMap = new ConcurrentHashMap<>();
    private final PushClientFactory androidClientFactory;
    private final PushClientFactory iosClientFactory;

    @Autowired
    public PushService(MessageDao messageDao, 
                       PushClientFactory androidClientFactory, 
                       PushClientFactory iosClientFactory) {
        this.messageDao = messageDao;
        this.androidClientFactory = androidClientFactory;
        this.iosClientFactory = iosClientFactory;
    }

    public void sendNotifications() {
        List<Message> messages = messageDao.getPendingMessages();
        for (Message msg : messages) {
            Pusher pusher = getPusherByOs(msg.getOsType());
            pusher.connect();
            pusher.sendNotification(msg.getToken(), msg.getContent());
            pusher.disconnect();
        }
    }

    private Pusher getPusherByOs(String osType) {
        return pusherMap.computeIfAbsent(osType, key -> {
            if ("android".equals(key)) {
                return new AndroidPusher(androidClientFactory);
            } else if ("ios".equals(key)) {
                return new IosPusher(iosClientFactory);
            }
            throw new IllegalArgumentException("不支持的系统: " + key);
        });
    }
}

测试Service时,只需Mock MessageDao返回测试数据,Mock PushClientFactory返回Mock的Pusher,就能完整模拟整个流程,完全不用接触真实推送服务器。

3. 让Pusher接口更聚焦(可选)

你当前每次发送都要手动调用connect/send/disconnect,其实可以考虑将连接逻辑封装到send方法内部,但你提到暂优先保证测试覆盖率、后续再优化连接逻辑,所以先保持现有接口即可。

三、Service的单元测试示例

@ExtendWith(MockitoExtension.class)
public class PushServiceTest {
    @Mock
    private MessageDao mockMessageDao;
    @Mock
    private PushClientFactory mockAndroidFactory;
    @Mock
    private PushClientFactory mockIosFactory;
    @Mock
    private Pusher mockAndroidPusher;
    @Mock
    private Pusher mockIosPusher;

    @InjectMocks
    private PushService pushService;

    @Test
    public void testSendNotifications() {
        // 模拟待发送消息
        Message androidMsg = new Message("android", "token1", "content1");
        Message iosMsg = new Message("ios", "token2", "content2");
        when(mockMessageDao.getPendingMessages()).thenReturn(List.of(androidMsg, iosMsg));

        // 让Service获取到Mock的Pusher实例
        PushService spyService = spy(pushService);
        doReturn(mockAndroidPusher).when(spyService).getPusherByOs("android");
        doReturn(mockIosPusher).when(spyService).getPusherByOs("ios");

        // 执行测试方法
        spyService.sendNotifications();

        // 验证每个Pusher的方法都按预期执行
        verify(mockAndroidPusher).connect();
        verify(mockAndroidPusher).sendNotification("token1", "content1");
        verify(mockAndroidPusher).disconnect();

        verify(mockIosPusher).connect();
        verify(mockIosPusher).sendNotification("token2", "content2");
        verify(mockIosPusher).disconnect();
    }
}

内容的提问来源于stack exchange,提问作者Pieter

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 12:44:58