如何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

