企业微信API接口对接中Java后端的模拟测试(Mock)与单元测试实战技巧
1. 企微API测试的典型难点
企业微信API调用具有强外部依赖:
- 需有效
access_token(有效期2小时); - 回调需公网可访问且签名校验;
- 消息发送、部门同步等操作不可逆。
直接调用真实接口会导致:测试不稳定、速率受限、污染生产数据。因此必须采用 分层Mock策略:HTTP层模拟 + 服务层隔离 + 回调事件注入。
2. 使用 WireMock 模拟企微HTTP接口
引入依赖:
xml
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
编写测试基类:
java
package wlkankan.cn.wechat.test;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
@TestPropertySource(properties = {
"wechat.qyapi.base-url=http://localhost:9999",
"wechat.corp-id=ww1234567890ab",
"wechat.corp-secret=SECRET_XXX"
})
public abstract class WeComWireMockBaseTest {
protected WireMockServer wireMockServer;
@BeforeEach
void startWireMock() {
wireMockServer = new WireMockServer(9999);
wireMockServer.start();
}
@AfterEach
void stopWireMock() {
if (wireMockServer != null) wireMockServer.stop();
}
}
模拟获取 access_token:
java
@Test
void testGetAccessToken() {
wireMockServer.stubFor(
post(urlEqualTo("/cgi-bin/gettoken"))
.withQueryParam("corpid", equalTo("ww1234567890ab"))
.withQueryParam("corpsecret", equalTo("SECRET_XXX"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{"errcode":0,"errmsg":"ok","access_token":"TEST_TOKEN_123","expires_in":7200}
"""))
);
String token = weComTokenService.getAccessToken();
assertEquals("TEST_TOKEN_123", token);
}

3. Mock 服务内部组件(Mockito)
对无法通过HTTP模拟的逻辑(如加解密、签名验证)使用 Mockito:
java
@ExtendWith(MockitoExtension.class)
class MessageServiceImplTest {
@Mock
private WeComTokenService tokenService;
@Mock
private WeComCrypto crypto;
@InjectMocks
private MessageServiceImpl messageService;
@Test
void sendTextMessage_success() {
when(tokenService.getAccessToken()).thenReturn("MOCK_TOKEN");
when(restTemplate.postForObject(anyString(), any(), eq(String.class)))
.thenReturn("{\"errcode\":0,\"errmsg\":\"ok\",\"msgid\":\"123456\"}");
SendResult result = messageService.sendText("user001", "Hello");
assertEquals("123456", result.getMsgId());
verify(restTemplate).postForObject(
eq("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=MOCK_TOKEN"),
argThat(req -> req.contains("user001") && req.contains("Hello")),
eq(String.class)
);
}
}
4. 模拟企微回调事件注入
企业微信回调为 XML 格式,测试时直接构造事件体:
java
@Test
void handleUserAddEvent() {
String xmlPayload = """
<xml>
<ToUserName><![CDATA[ww1234567890ab]]></ToUserName>
<FromUserName><![CDATA[sys]]></FromUserName>
<CreateTime>1700000000</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[change_contact]]></Event>
<ChangeType><![CDATA[create_user]]></ChangeType>
<UserID><![CDATA[zhangsan]]></UserID>
<Name><![CDATA[张三]]></Name>
</xml>
""";
// 跳过签名校验(开发配置)
ResponseEntity<String> response = restTemplate.postForEntity(
"/callback/wecom/ww1234567890ab?msg_signature=xxx×tamp=1700000000&nonce=abcd",
xmlPayload,
String.class
);
assertEquals("success", response.getBody());
// 验证用户同步服务被调用
verify(userSyncService).onUserCreated(eq("zhangsan"), eq("张三"));
}
5. 使用 Testcontainers 模拟数据库状态
若涉及设备绑定、消息记录等持久化操作,使用内存数据库:
java
@SpringBootTest
@Testcontainers
class DeviceBindServiceTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private DeviceBindService deviceBindService;
@Test
void bindDevice_success() {
WeComDevice device = new WeComDevice();
device.setCorpId("ww1234567890ab");
device.setAgentId(1000001L);
device.setStatus(DeviceStatus.PENDING);
deviceBindService.bind(device, "CODE_FROM_QR");
// 验证状态更新
WeComDevice saved = deviceRepository.findById(device.getId()).orElseThrow();
assertEquals(DeviceStatus.ACTIVE, saved.getStatus());
assertNotNull(saved.getAccessToken());
}
}
6. 集成测试中的安全凭证隔离
通过 @TestConfiguration 覆盖生产配置:
java
@TestConfiguration
static class TestWeComConfig {
@Bean
@Primary
public RestTemplate testRestTemplate() {
RestTemplate rt = new RestTemplate();
// 禁用 SSL 验证(仅测试)
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(HttpClients.custom()
.setSSLHostnameVerifier((s, sslSession) -> true)
.build());
rt.setRequestFactory(factory);
return rt;
}
}
7. 测试覆盖率保障
在 pom.xml 中集成 JaCoCo:
xml
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
执行 mvn test jacoco:report 生成覆盖率报告,确保核心路径(如 token 刷新、消息重试、回调解析)覆盖率达 90% 以上。
通过 WireMock 模拟外部 HTTP、Mockito 隔离内部依赖、Testcontainers 管理状态、回调事件注入与配置隔离,可在无网络、无真实企微账号环境下完成高可靠、可重复的企业微信 API 对接测试。