使用Mockito与WireMock对美团霸王餐接口进行契约测试与集成验证

使用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开发者团队,转载请注明出处!

相关推荐
wangbing112527 分钟前
springboot应用关闭时触发动作
java·spring boot·后端
weixin_460443564 小时前
2026年新修订《网络安全法》实施:Java私有化考试系统如何做好SBOM、漏洞治理与安全升级?
java·开发语言·在线考试系统·企业培训考试系统·宏远培训考试系统·国企培训考试系统·国产化培训系统
海鸥-w7 小时前
什么是AOP
java
Cc.Y7 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck7 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787057 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev7 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
Sarvartha7 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞7 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计
colinu8 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++