苍穹外卖接口测试项目(基于 Java+TestNG+OkHttp)搭建与维护指南

苍穹外卖接口测试项目(基于 Java+TestNG+OkHttp)搭建与维护指南

前言

本文记录苍穹外卖接口测试项目从「环境准备→项目搭建→问题排查→Gitee 上传→后续扩展」的完整流程,适用于接口测试新手,可直接复用代码并扩展其他接口测试(如员工管理、订单管理等)。

1. 项目背景与目标

1.1 背景

基于「苍穹外卖」后端服务,实现接口自动化测试,验证核心业务流程(如员工登录、退出、订单查询等)的正确性。

1.2 技术栈

  • 开发语言:Java 21(兼容 Java 8+)
  • 构建工具:Maven 3.6+
  • 测试框架:TestNG(用例管理、断言、执行)
  • 网络请求:OkHttp(发送 HTTP 请求,Swagger 生成的 ApiClient 底层依赖)
  • 配置管理:YAML(存储环境配置、账号密码等)
  • 数据解析:Gson(JSON 序列化/反序列化)
  • 依赖管理:Lombok(简化实体类代码)
  • 版本控制:Git + Gitee(代码托管与维护)

1.3 核心目标

  • 实现员工登录接口的正向/反向测试;
  • 支持多环境配置(开发、测试、生产);
  • 代码可复用、易扩展(新增接口测试无需重复写基础代码);
  • 代码托管到 Gitee,方便团队协作与维护。

2. 环境准备(必做)

2.1 基础环境

工具版本要求下载地址
JDK8+(本文用 21)Oracle JDK
Maven3.6+Maven 官网
IDEA2021+IDEA 官网
Git任意稳定版Git 官网
Gitee 账号注册即可Gitee 官网

2.2 环境配置验证

打开命令行执行以下命令,无报错则配置成功:

java -version  # 显示 JDK 版本
mvn -v         # 显示 Maven 版本
git --version  # 显示 Git 版本

3. 项目搭建步骤(从零开始)

3.1 步骤 1:创建 Maven 项目

  1. 打开 IDEA → 「New Project」→ 选择「Maven」→ 取消勾选「Create from archetype」→ 「Next」;
  2. 填写项目信息:
    • Name:sky-takeout-test(项目名称);
    • Location:E:\code\sky-takeout-test(项目存储路径);
    • GroupId:com.sky.test(包名前缀);
    • ArtifactId:sky-takeout-test
    • Version:1.0-SNAPSHOT
  3. 点击「Finish」,等待 Maven 初始化完成(右下角进度条消失)。

3.2 步骤 2:引入依赖(修改 pom.xml)

pom.xml 中添加以下依赖,刷新 Maven 下载(IDEA 右侧「Maven」→ 「Reload All Maven Projects」):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sky.test</groupId>
    <artifactId>sky-takeout-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 依赖管理 -->
    <dependencies>
        <!-- TestNG 测试框架 -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.10.2</version>
            <scope>test</scope>
        </dependency>

        <!-- OkHttp 网络请求 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.12.0</version>
        </dependency>

        <!-- Gson JSON 解析 -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>

        <!-- Lombok 简化实体类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.36</version>
            <optional>true</optional>
        </dependency>

        <!-- SnakeYAML 解析 YAML 配置 -->
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>2.2</version>
        </dependency>

        <!-- Swagger Codegen 生成 ApiClient(如果用 Swagger 文档) -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-codegen-cli</artifactId>
            <version>3.0.46</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <!-- 构建配置(可选,用于指定 JDK 版本) -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                    <encoding>UTF-8</encoding>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.36</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3 步骤 3:编写配置文件(YAML)

  1. src/main/resources 目录下创建 2 个 YAML 文件:

    • application.yml(主配置,指定激活环境);
    • application-dev.yml(开发环境配置,存储接口地址、账号密码)。
  2. 配置文件内容:

    • application.yml
      spring:
        profiles:
          active: dev  # 激活开发环境(后续可切换为 test/prod)
      
    • application-dev.yml
      sky:
        api:
          baseUrl: http://localhost:8080  # 后端服务地址
          adminPrefix: /admin             # 管理员接口前缀
        login:
          username: admin                 # 测试账号
          password: 123456                # 测试密码
      

3.4 步骤 4:编写核心工具类

4.1 配置解析工具:YamlConfigLoader.java

作用:读取 YAML 配置文件,提供全局访问配置的方法。

package com.sky.test.config;

import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Optional;

public class YamlConfigLoader {
    private static Config config;

    // 静态代码块:项目启动时加载配置
    static {
        loadConfig();
    }

    private static void loadConfig() {
        Yaml yaml = new Yaml();
        // 1. 读取主配置,获取激活环境
        String activeEnv = "dev";
        try (InputStream mainStream = YamlConfigLoader.class.getClassLoader().getResourceAsStream("application.yml")) {
            if (mainStream == null) throw new RuntimeException("找不到 application.yml 配置文件");
            SpringProfiles springProfiles = yaml.loadAs(mainStream, SpringProfiles.class);
            if (springProfiles != null && springProfiles.getSpring() != null 
                    && springProfiles.getSpring().getProfiles() != null) {
                activeEnv = springProfiles.getSpring().getProfiles().getActive();
            }
        } catch (Exception e) {
            throw new RuntimeException("读取主配置失败:" + e.getMessage(), e);
        }

        // 2. 读取激活环境的配置(如 application-dev.yml)
        String envConfigFile = "application-" + activeEnv + ".yml";
        try (InputStream envStream = YamlConfigLoader.class.getClassLoader().getResourceAsStream(envConfigFile)) {
            if (envStream == null) throw new RuntimeException("找不到环境配置文件:" + envConfigFile);
            config = yaml.loadAs(envStream, Config.class);
        } catch (Exception e) {
            throw new RuntimeException("读取" + activeEnv + "环境配置失败:" + e.getMessage(), e);
        }

        // 3. 校验配置完整性(避免空指针)
        validateConfig();
    }

    private static void validateConfig() {
        Optional.ofNullable(config).orElseThrow(() -> new RuntimeException("配置解析失败,未获取到有效数据"));
        Optional.ofNullable(config.getSky()).orElseThrow(() -> new RuntimeException("配置缺少 sky 节点"));
        Optional.ofNullable(config.getSky().getApi()).orElseThrow(() -> new RuntimeException("配置缺少 sky.api 节点"));
        Optional.ofNullable(config.getSky().getApi().getBaseUrl()).orElseThrow(() -> new RuntimeException("配置缺少 sky.api.baseUrl"));
        Optional.ofNullable(config.getSky().getLogin()).orElseThrow(() -> new RuntimeException("配置缺少 sky.login 节点"));
    }

    // 提供外部访问配置的方法
    public static Config getConfig() {
        return config;
    }

    // 静态内部类:对应 application.yml 中的 spring 节点
    @lombok.Data
    public static class SpringProfiles {
        private Spring spring;

        @lombok.Data
        public static class Spring {
            private Profiles profiles;

            @lombok.Data
            public static class Profiles {
                private String active;
            }
        }
    }
}
4.2 配置实体类:Config.java

作用:映射 YAML 配置文件的结构(字段名与 YAML 键名一致)。

package com.sky.test.config;

import lombok.Data;

@Data
public class Config {
    private Sky sky;

    @Data
    public static class Sky {
        private Api api;
        private Login login;

        @Data
        public static class Api {
            private String baseUrl;
            private String adminPrefix;
        }

        @Data
        public static class Login {
            private String username;
            private String password;
        }
    }
}

3.5 步骤 5:编写测试基类(BaseTest.java)

作用:复用初始化逻辑(加载配置、初始化 ApiClient),所有测试用例都继承此类。

package com.sky.test.cases.base;

import com.sky.test.ApiClient;
import com.sky.test.config.Config;
import com.sky.test.config.YamlConfigLoader;

public class BaseTest {
    protected ApiClient apiClient;  // Swagger 生成的 ApiClient(或直接用 OkHttpClient)
    protected String BASE_URL;       // 接口基础地址
    protected String ADMIN_PREFIX;   // 管理员接口前缀
    protected String LOGIN_USERNAME; // 测试账号
    protected String LOGIN_PASSWORD; // 测试密码
    protected String token;          // 登录成功后存储 Token(供后续接口使用)

    // TestNG 注解:每个测试类执行前初始化
    @org.testng.annotations.BeforeClass
    public void initApiClient() {
        // 1. 读取配置
        Config config = YamlConfigLoader.getConfig();
        BASE_URL = config.getSky().getApi().getBaseUrl();
        ADMIN_PREFIX = config.getSky().getApi().getAdminPrefix();
        LOGIN_USERNAME = config.getSky().getLogin().getUsername();
        LOGIN_PASSWORD = config.getSky().getLogin().getPassword();

        // 2. 初始化 ApiClient(Swagger 生成,或直接 new OkHttpClient())
        apiClient = new ApiClient();
        apiClient.setBasePath(BASE_URL + ADMIN_PREFIX); // 拼接完整接口路径

        // 3. 添加默认请求头(JSON 接口必需)
        apiClient.addDefaultHeader("Content-Type", "application/json");

        // 打印初始化信息(验证配置是否正确)
        System.out.println("=======================================");
        System.out.println("=== 测试套件初始化成功 ===");
        System.out.println("激活环境:dev");
        System.out.println("完整接口路径:" + BASE_URL + ADMIN_PREFIX);
        System.out.println("登录用户名:" + LOGIN_USERNAME);
        System.out.println("=======================================");
    }
}

3.6 步骤 6:编写测试用例(员工登录)

6.1 实体类(对应接口请求/响应格式)
  • 请求实体:EmployeeLoginDTO.java(登录请求参数)
    package com.sky.test.api.model;
    
    import lombok.Data;
    
    @Data
    public class EmployeeLoginDTO {
        private String username; // 与后端接口请求字段名一致
        private String password; // 与后端接口请求字段名一致
    }
    
  • 响应实体:ResultEmployeeLoginVO.java(登录响应结果)
    package com.sky.test.api.model;
    
    import lombok.Data;
    
    @Data
    public class ResultEmployeeLoginVO {
        private Integer code;    // 响应码(1=成功,0=失败)
        private String msg;      // 提示信息
        private EmployeeData data; // 成功后返回的用户信息(包含 Token)
    
        @Data
        public static class EmployeeData {
            private String token; // 登录 Token
            // 其他字段(如 id、username 等,根据后端返回补充)
        }
    }
    
6.2 测试用例类:EmployeeLoginTest.java

包含正向测试(正确账号密码)和反向测试(错误密码)。

package com.sky.test.cases;

import com.sky.test.api.model.EmployeeLoginDTO;
import com.sky.test.api.model.ResultEmployeeLoginVO;
import com.sky.test.cases.base.BaseTest;
import okhttp3.Call;
import okhttp3.Response;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.HashMap;

public class EmployeeLoginTest extends BaseTest {

    @Test(description = "员工登录-正向测试:正确账号密码登录成功")
    public void testLoginSuccess() throws Exception {
        // 1. 构建请求路径(拼接基础路径+接口路径)
        String path = "/employee/login";

        // 2. 构建请求体(传入测试账号密码)
        EmployeeLoginDTO loginDTO = new EmployeeLoginDTO();
        loginDTO.setUsername(LOGIN_USERNAME);
        loginDTO.setPassword(LOGIN_PASSWORD);

        // 3. 调用 ApiClient 构建请求(关键:headerParams 传空 Map,避免空指针)
        Call call = apiClient.buildCall(
                path,
                "POST",
                null,
                null,
                loginDTO,
                new HashMap<>(), // 必须传空 Map,不能传 null
                null,
                new String[]{},
                null
        );

        // 4. 执行请求并获取响应
        Response response = call.execute();
        String responseBody = response.body().string();

        // 5. 解析响应(JSON → Java 实体类)
        ResultEmployeeLoginVO loginVO = apiClient.getJSON().deserialize(responseBody, ResultEmployeeLoginVO.class);

        // 6. 断言验证(根据后端实际返回调整预期值)
        Assert.assertEquals(loginVO.getCode(), 1, "响应码应为 1(成功),实际为:" + loginVO.getCode());
        Assert.assertNotNull(loginVO.getData(), "登录成功应返回用户信息(data 不为 null)");
        Assert.assertNotNull(loginVO.getData().getToken(), "登录成功应返回 Token");
        Assert.assertEquals(loginVO.getMsg(), "员工登录成功", "提示信息应为「员工登录成功」,实际为:" + loginVO.getMsg());

        // 7. 存储 Token(供后续接口测试使用,如查询员工列表)
        token = loginVO.getData().getToken();
        System.out.println("=== 登录成功 ===");
        System.out.println("Token:" + token);
        System.out.println("响应体:" + responseBody);
    }

    @Test(description = "员工登录-反向测试:错误密码登录失败", dependsOnMethods = "testLoginSuccess")
    public void testLoginFailWithWrongPwd() throws Exception {
        String path = "/employee/login";
        EmployeeLoginDTO loginDTO = new EmployeeLoginDTO();
        loginDTO.setUsername(LOGIN_USERNAME);
        loginDTO.setPassword("wrong123456"); // 错误密码

        // 构建并执行请求
        Call call = apiClient.buildCall(
                path, "POST", null, null, loginDTO,
                new HashMap<>(), null, new String[]{}, null
        );
        Response response = call.execute();
        String responseBody = response.body().string();
        ResultEmployeeLoginVO loginVO = apiClient.getJSON().deserialize(responseBody, ResultEmployeeLoginVO.class);

        // 断言失败结果
        Assert.assertEquals(loginVO.getCode(), 0, "响应码应为 0(失败),实际为:" + loginVO.getCode());
        Assert.assertNull(loginVO.getData(), "登录失败不应返回用户信息(data 为 null)");
        Assert.assertTrue(loginVO.getMsg().contains("密码错误"), "提示信息应包含「密码错误」,实际为:" + loginVO.getMsg());

        System.out.println("=== 错误密码登录测试通过 ===");
        System.out.println("响应体:" + responseBody);
    }
}

3.7 步骤 7:执行测试用例

  1. 确保苍穹外卖后端服务已启动(访问 http://localhost:8080 可打开页面);
  2. 右键 EmployeeLoginTest.java → 「Run ‘EmployeeLoginTest’」;
  3. 查看执行结果:
    • 控制台打印「登录成功」和 Token → 正向用例通过;
    • 反向用例打印「错误密码登录测试通过」→ 反向用例通过;
    • IDEA 底部「Run」面板显示「2 tests passed」→ 全部通过。

4. 常见问题与排查(踩坑记录)

问题 1:YAML 解析报错「Unable to find property ‘base-url’」

  • 原因:YAML 键名与 Config 类字段名不一致(如 YAML 用 base-url,Config 用 baseUrl);
  • 解决方案:统一为驼峰命名(YAML 也用 baseUrl),或使用 SnakeYAML 自带注解(复杂,不推荐)。

问题 2:Lombok 报错「NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field ‘qualid’」

  • 原因:Lombok 版本过低,不兼容 JDK 17+;
  • 解决方案:升级 Lombok 到 1.18.30+(本文用 1.18.36),并开启 IDEA 注解处理器。

问题 3:headerParams 空指针「Cannot invoke ‘java.util.Map.entrySet()’」

  • 原因:调用 apiClient.buildCall() 时,第 6 个参数 headerParams 传了 null,ApiClient 遍历时空指针;
  • 解决方案:传空 Map new HashMap<>(),而非 null

问题 4:断言失败「提示信息应为’操作成功’,实际为’员工登录成功’」

  • 原因:预期提示信息与后端实际返回不一致;
  • 解决方案:查看接口实际响应(打印 responseBody),调整断言的预期值。

5. Gitee 仓库维护(代码托管与更新)

5.1 首次上传仓库

  1. 访问 Gitee 官网,登录账号 → 右上角「+」→ 「新建仓库」;
  2. 填写仓库信息:仓库名称(如 sky-takeout-test)、可见性(私有/公开),取消勾选「初始化 README」等 → 「创建仓库」;
  3. 本地项目根目录打开终端,执行以下命令:
    # 初始化本地 Git 仓库
    git init
    # 添加所有文件到暂存区
    git add .
    # 提交到本地仓库
    git commit -m "苍穹外卖测试项目-初始化:员工登录接口测试"
    # 关联 Gitee 远程仓库(替换为你的仓库 HTTPS 地址)
    git remote add origin https://gitee.com/你的用户名/sky-takeout-test.git
    # 推送至 Gitee(首次推送用 -u 绑定分支)
    git push -u origin master
    
  4. 输入 Gitee 账号密码/Token,推送成功后刷新 Gitee 仓库即可查看代码。

5.2 后续更新代码(修改/新增功能后)

# 1. 添加修改的文件到暂存区
git add .
# 2. 提交代码(备注清晰修改内容)
git commit -m "新增员工退出接口测试"
# 3. 推送到 Gitee
git push origin master

5.3 仓库分支管理(可选,多人协作时使用)

  • 主分支:master(存储稳定可运行的代码);
  • 开发分支:dev(开发新功能时创建,测试通过后合并到 master);
  • 命令示例:
    # 创建并切换到 dev 分支
    git checkout -b dev
    # 开发完成后合并到 master
    git checkout master
    git merge dev
    git push origin master
    

6. 后续扩展思路(项目优化与新增功能)

6.1 新增接口测试(如员工退出、订单查询)

  1. 新增接口请求/响应实体类(如 EmployeeLogoutDTOResultEmployeeLogoutVO);
  2. 新建测试类 EmployeeLogoutTest.java,继承 BaseTest
  3. 复用 token(登录成功后存储的 Token),在请求头中添加 Authorization: Bearer ${token}(后端需要 Token 验证时);
  4. 编写测试用例,执行并验证。

6.2 数据驱动测试(多组测试数据)

  • 需求:用多组账号密码测试登录(正确账号、空用户名、空密码、错误账号等);
  • 实现:使用 TestNG 的 @DataProvider 注解,提供多组测试数据:
    // 示例:数据驱动登录测试
    @DataProvider(name = "loginData")
    public Object[][] loginData() {
        return new Object[][]{
            // {username, password, expectedCode, expectedMsg}
            {LOGIN_USERNAME, LOGIN_PASSWORD, 1, "员工登录成功"}, // 正确账号密码
            {LOGIN_USERNAME, "", 0, "密码不能为空"},              // 空密码
            {"", LOGIN_PASSWORD, 0, "用户名不能为空"},            // 空用户名
            {"wrongAdmin", "123456", 0, "账号不存在"}             // 错误账号
        };
    }
    
    // 引用数据驱动
    @Test(description = "员工登录-数据驱动测试", dataProvider = "loginData")
    public void testLoginDataDriven(String username, String password, int expectedCode, String expectedMsg) throws Exception {
        // 构建请求体
        EmployeeLoginDTO loginDTO = new EmployeeLoginDTO();
        loginDTO.setUsername(username);
        loginDTO.setPassword(password);
    
        // 执行请求、解析响应、断言(逻辑复用之前的代码)
        Call call = apiClient.buildCall(path, "POST", null, null, loginDTO, new HashMap<>(), null, new String[]{}, null);
        Response response = call.execute();
        String responseBody = response.body().string();
        ResultEmployeeLoginVO loginVO = apiClient.getJSON().deserialize(responseBody, ResultEmployeeLoginVO.class);
    
        Assert.assertEquals(loginVO.getCode(), expectedCode);
        Assert.assertTrue(loginVO.getMsg().contains(expectedMsg));
    }
    

6.3 生成测试报告

  • 需求:执行完测试后,生成可视化报告(展示用例执行结果、通过率等);
  • 实现:引入 TestNG 报告插件(如 testng-reportng)或 Allure 报告,配置后执行测试即可生成。

6.4 CI/CD 集成(进阶)

  • 需求:提交代码到 Gitee 后,自动执行测试用例,生成报告并通知结果;
  • 实现:使用 Gitee CI/CD 或 Jenkins,配置构建脚本(拉取代码、执行 mvn test、生成报告)。

总结

本项目从环境准备到测试用例执行,再到 Gitee 维护,形成了完整的接口自动化测试流程。核心优势是「代码复用性高」(BaseTest 封装初始化逻辑)、「配置灵活」(YAML 支持多环境)、「易扩展」(新增接口测试无需重复写基础代码)。

后续可根据业务需求,逐步扩展其他接口测试(如订单管理、菜品管理等),并通过数据驱动、测试报告、CI/CD 等优化,提升测试效率和项目稳定性。

posted @ 2025-11-09 19:49  WILK  阅读(0)  评论(0)    收藏  举报  来源