使用Mockito与WireMock对美团霸王餐接口进行契约测试与集成验证
在外卖平台对接美团"霸王餐"营销接口时,依赖外部服务的稳定性直接影响开发效率与系统可靠性。为在不依赖真实环境的前提下验证客户端逻辑、异常处理及协议兼容性,需采用契约测试(Contract Testing) 与集成测试(Integration Testing) 相结合的方式。本文通过 Mockito 模拟内部服务依赖,结合 WireMock 模拟美团远程HTTP接口,在 baodanbao.com.cn.* 包结构下构建可维护、高覆盖的测试体系。
项目依赖配置
在 pom.xml 中引入必要测试依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

定义美团霸王餐Feign客户端
首先定义对接美团的Feign接口:
java
package baodanbao.com.cn.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "meituan-free-meal", url = "${meituan.api.url}")
public interface MeituanFreeMealClient {
@PostMapping("/v1/free-meal/claim")
ClaimResponse claim(@RequestBody ClaimRequest request,
@RequestHeader("Authorization") String token);
}
其中 ClaimRequest 和 ClaimResponse 位于 baodanbao.com.cn.dto 包。
使用WireMock模拟美团API
创建集成测试类,启动WireMock服务模拟美团端点:
java
package baodanbao.com.cn.integration;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import baodanbao.com.cn.client.MeituanFreeMealClient;
import baodanbao.com.cn.dto.ClaimRequest;
import baodanbao.com.cn.dto.ClaimResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@TestPropertySource(properties = {
"meituan.api.url=http://localhost:9999"
})
public class MeituanFreeMealIntegrationTest {
private WireMockServer wireMockServer;
@Autowired
private MeituanFreeMealClient client;
@BeforeEach
void setup() {
wireMockServer = new WireMockServer(9999);
wireMockServer.start();
configureFor("localhost", 9999);
}
@AfterEach
void teardown() {
wireMockServer.stop();
}
@Test
void shouldSuccessfullyClaimFreeMeal() {
// 模拟美团成功响应
stubFor(post(urlEqualTo("/v1/free-meal/claim"))
.withRequestBody(containing("userId"))
.withHeader("Authorization", containing("Bearer"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{"success":true,"couponCode":"MT2025FREE","message":"ok"}
""")));
ClaimRequest request = new ClaimRequest();
request.setUserId(1001L);
request.setActivityId("ACT_2025");
ClaimResponse response = client.claim(request, "Bearer fake-token");
assertThat(response.isSuccess()).isTrue();
assertThat(response.getCouponCode()).isEqualTo("MT2025FREE");
verify(postRequestedFor(urlEqualTo("/v1/free-meal/claim")));
}
@Test
void shouldHandleMeituanApiError() {
stubFor(post(urlEqualTo("/v1/free-meal/claim"))
.willReturn(aResponse().withStatus(500)));
ClaimRequest request = new ClaimRequest();
request.setUserId(1002L);
request.setActivityId("ACT_2025");
// Feign默认抛出FeignException
org.springframework.web.client.HttpServerErrorException exception =
org.junit.jupiter.api.assertThrows(
org.springframework.web.client.HttpServerErrorException.class,
() -> client.claim(request, "Bearer fake-token")
);
assertThat(exception.getStatusCode().value()).isEqualTo(500);
}
}
使用Mockito模拟内部服务依赖
在更高层的服务测试中,需隔离内部组件(如用户服务、日志服务),仅聚焦核心逻辑:
java
package baodanbao.com.cn.service;
import baodanbao.com.cn.client.MeituanFreeMealClient;
import baodanbao.com.cn.dto.ClaimRequest;
import baodanbao.com.cn.dto.ClaimResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
public class FreeMealServiceTest {
@Mock
private MeituanFreeMealClient meituanClient;
@Mock
private AuditLogService auditLogService; // 内部日志服务
@InjectMocks
private FreeMealService freeMealService;
@Test
void shouldClaimAndLogSuccess() {
ClaimResponse mockResponse = new ClaimResponse();
mockResponse.setSuccess(true);
mockResponse.setCouponCode("MT_MOCK_001");
when(meituanClient.claim(any(ClaimRequest.class), any(String.class)))
.thenReturn(mockResponse);
// 不验证auditLogService具体行为,但确保未抛异常
ClaimResult result = freeMealService.claimForUser(2001L, "ACT_2025", "token123");
assertThat(result.isSuccess()).isTrue();
assertThat(result.getCouponCode()).isEqualTo("MT_MOCK_001");
}
}
其中 FreeMealService 为业务服务类:
java
package baodanbao.com.cn.service;
import baodanbao.com.cn.client.MeituanFreeMealClient;
import baodanbao.com.cn.dto.ClaimRequest;
import baodanbao.com.cn.dto.ClaimResponse;
import org.springframework.stereotype.Service;
@Service
public class FreeMealService {
private final MeituanFreeMealClient meituanClient;
private final AuditLogService auditLogService;
public FreeMealService(MeituanFreeMealClient meituanClient, AuditLogService auditLogService) {
this.meituanClient = meituanClient;
this.auditLogService = auditLogService;
}
public ClaimResult claimForUser(Long userId, String activityId, String token) {
ClaimRequest request = new ClaimRequest();
request.setUserId(userId);
request.setActivityId(activityId);
ClaimResponse response = meituanClient.claim(request, "Bearer " + token);
auditLogService.logClaim(userId, activityId, response.isSuccess());
return new ClaimResult(response.isSuccess(), response.getCouponCode());
}
}
契约测试:保障接口兼容性
通过WireMock录制真实美团响应(或根据OpenAPI文档编写),可作为团队共享的契约。若美团接口变更导致请求格式不匹配,测试将立即失败,提前暴露集成风险。
本文著作权归吃喝不愁app开发者团队,转载请注明出处!