Java中Mock SOAP调用返回空响应的问题排查与解决
我之前也碰到过类似的情况——SOAPMessage的对象匹配坑真的挺烦人的,毕竟默认的equals根本不会按XML内容去比较。咱们一步步来排查和解决:
第一步:先搞清楚请求到底哪里不一样
你说改了时间戳还是不行,那得先精准定位差异点。咱们把业务代码里生成的payload和测试里的testPayload都转成字符串,直接对比内容:
先写个工具方法把SOAPMessage转成字符串:
private String soapMessageToString(SOAPMessage msg) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); msg.writeTo(baos); return baos.toString(); }
然后在业务代码里临时加日志(或者调试时打印):
SOAPMessage payload = getAddressRequestPayload(); System.out.println("业务请求XML:" + soapMessageToString(payload)); // 临时打印 SOAPMessage responseMessage = soapConnection.call(payload, settings.addressUrl());
测试代码里也打印测试用的请求:
SOAPMessage testPayload = abc.getAddressRequestPayload(); System.out.println("测试请求XML:" + soapMessageToString(testPayload)); // 临时打印 SOAPMessage testResponse = getMockResponse();
对比这两个字符串,你就能找到差异了——可能是这些地方:
- 时间戳的毫秒级差异(比如你改了时间但没精确到毫秒)
- 自动生成的请求ID、UUID之类的随机值
- XML里的空格、换行符差异(比如业务代码生成的XML有缩进,测试里的没有)
- 命名空间的前缀不一致(比如业务里是
fedex:,测试里是ns1:)
第二步:针对性解决匹配问题
根据找到的差异,选对应的方案:
方案1:把动态生成的部分抽成可Mock的依赖
如果差异是时间戳、随机ID这类动态值,别在getAddressRequestPayload()里直接生成,把这部分逻辑抽成单独的类,比如:
// 业务代码里新增依赖 public interface RequestIdGenerator { String generateId(); } public interface TimestampProvider { String getCurrentTimestamp(); } // 在abc.java里注入这些依赖,生成请求时调用它们 SOAPMessage getAddressRequestPayload() { String requestId = requestIdGenerator.generateId(); String timestamp = timestampProvider.getCurrentTimestamp(); // 用这两个值构建SOAP请求 }
测试时Mock这些依赖,让它们返回固定值:
when(requestIdGenerator.generateId()).thenReturn("fixed-test-id"); when(timestampProvider.getCurrentTimestamp()).thenReturn("2024-05-20T12:00:00Z");
这样业务代码和测试代码生成的请求就能完全一致,Mock的匹配自然就生效了。
方案2:用自定义ArgumentMatcher匹配SOAP内容
如果不想改业务代码,或者差异是XML格式上的小问题(比如空格),可以用Mockito的自定义匹配器,按XML内容来判断是否匹配,而不是依赖SOAPMessage的equals:
先写个匹配器类:
class SOAPContentMatcher extends ArgumentMatcher<SOAPMessage> { private final SOAPMessage expectedMsg; public SOAPContentMatcher(SOAPMessage expectedMsg) { this.expectedMsg = expectedMsg; } @Override public boolean matches(Object argument) { if (!(argument instanceof SOAPMessage)) { return false; } try { // 转成字符串 String expectedXml = soapMessageToString(expectedMsg); String actualXml = soapMessageToString((SOAPMessage) argument); // 忽略无关差异,比如时间戳、请求ID expectedXml = expectedXml.replaceAll("<RequestTimestamp>.*?</RequestTimestamp>", "<RequestTimestamp>IGNORED</RequestTimestamp>"); expectedXml = expectedXml.replaceAll("<TransactionId>.*?</TransactionId>", "<TransactionId>IGNORED</TransactionId>"); actualXml = actualXml.replaceAll("<RequestTimestamp>.*?</RequestTimestamp>", "<RequestTimestamp>IGNORED</RequestTimestamp>"); actualXml = actualXml.replaceAll("<TransactionId>.*?</TransactionId>", "<TransactionId>IGNORED</TransactionId>"); // 还可以去掉所有空格、换行符,避免格式差异 expectedXml = expectedXml.replaceAll("\\s+", ""); actualXml = actualXml.replaceAll("\\s+", ""); return expectedXml.equals(actualXml); } catch (Exception e) { e.printStackTrace(); return false; } } }
然后Mock的时候用这个匹配器:
when(mockSoapConnection.call( argThat(new SOAPContentMatcher(testPayload)), eq(settings.addressUrl()) )).thenReturn(testResponse);
方案3:用ArgumentCaptor验证请求内容
如果不想提前匹配,而是想验证实际调用的请求是否符合预期,可以用Mockito的ArgumentCaptor捕获实际参数,然后在测试里验证:
// 定义Captor ArgumentCaptor<SOAPMessage> payloadCaptor = ArgumentCaptor.forClass(SOAPMessage.class); // 执行你的业务方法 abc.doAddressValidation(); // 捕获调用的SOAPMessage参数 verify(mockSoapConnection).call(payloadCaptor.capture(), eq(settings.addressUrl())); // 验证捕获的请求内容 SOAPMessage actualPayload = payloadCaptor.getValue(); String actualXml = soapMessageToString(actualPayload); String expectedXml = soapMessageToString(testPayload); // 同样忽略无关差异后对比 expectedXml = expectedXml.replaceAll("<RequestTimestamp>.*?</RequestTimestamp>", "<RequestTimestamp>IGNORED</RequestTimestamp>"); actualXml = actualXml.replaceAll("<RequestTimestamp>.*?</RequestTimestamp>", "<RequestTimestamp>IGNORED</RequestTimestamp>"); assertEquals(expectedXml.replaceAll("\\s+", ""), actualXml.replaceAll("\\s+", ""));
额外小建议
如果经常要测SOAP服务,试试WireMock这类工具——它可以模拟整个SOAP服务端点,你只需要配置好请求匹配规则,就能验证业务代码发送的SOAP请求是否正确,比Mock SOAPConnection更贴近真实场景。
内容的提问来源于stack exchange,提问作者Saurabh Dwivedi

