jck28-lucio-【实战】电子商城接口自动化测试实战

目录

  • 接口自动化测试使用场景
  • 产品需求分析
  • 测试用例设计思路
  • 接口自动化脚本编写

接口自动化测试使用场景

participant 需求
participant 设计
participant 开发
participant 测试
participant 预发布
participant 验收
participant 上线
scale 1000
autonumber

需求 → 设计:…
设计 → 开发:…
开发 → 测试: 冒烟测试
note left : 主流程自动化测试
测试 → 预发布:…
预发布 → 验收: 验收测试
note left : 全量自动化测试
验收 → 上线:…

接口自动化测试使用场景

电子商城产品图片

image

电子商城需求分析

商城业务场景

  • 商品上架
  • 商品查询
  • 加入购物车

scale 700*500
autonumber
participant 测试人员 as tester
participant 登录模块 as login
participant 管理后台 as admin
participant 客户端 as customer

tester → login: 编写脚本
login → admin: 登录接口
admin → customer: 商品上架接口
admin → customer: 商品查询接口
customer → customer: 加购接口
customer → tester: 测试断言

研发技术评审

  • 管理后台接口文档

https://litemall.hogwarts.ceshiren.com/swagger-ui.html#/

接口测试用例设计思路

@startmindmap
*[#Orange] 接口测试思路
**[#lightblue] 基本功能流程测试(p1)
***[#lightgreen] 冒烟测试
***[#lightgreen] 正常流程覆盖测试
**[#lightblue] 基于输入域的测试(p2)
***[#lightgreen] 边界值测试
***[#lightgreen] 特殊字符校验
***[#lightgreen] 参数类型校验
***[#lightgreen] 必选参数校验
***[#lightgreen] 组合参数校验
***[#lightgreen] 有效性校验
***[#lightgreen] 默认值校验
***[#lightgreen] 排重逻辑

left side

**[#lightblue] 接口幂等性
***[#lightgreen] 重复提交
**[#lightblue] 故障注入
***[#lightgreen] Redis故障降级测试
***[#lightgreen] 服务故障转移测试
**[#lightblue] 线程安全测试
***[#lightgreen] 并发测试
***[#lightgreen] 分布式测试
***[#lightgreen] 数据库读写安全测试
@endmindmap

添加购物车流程脚本编写

title 编写思路
@startmindmap

  • 思路
    ** 获取接口信息
    *** swagger 接口文档
    *** 前端抓包
    ** 单步调通接口后,根据业务流程串联起来
    ** 添加断言,确认流程正常
    @endmindmap
  1. 调用上架商品接口,上架商品。
  2. 调用查询商品列表接口,获取商品ID
  3. 查询商品详情接口,获取商品库存ID
  4. 调用加入购物车接口,添加购物车

环境依赖(pom.xml)

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>11</java.version>
        <!-- 使用 Java 11 语言特性 ( -source 11 ) 并且还希望编译后的类与 JVM 11 ( -target 11 )兼容,您可以添加以下两个属性,它们是默认属性插件参数的名称-->
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.source>11</maven.compiler.source>
        <!-- 对应junit Jupiter的版本号;放在这里就不需要在每个依赖里面写版本号,导致对应版本号会冲突-->
        <junit.jupiter.version>5.8.2</junit.jupiter.version>

        <maven.compiler.version>3.8.1</maven.compiler.version>
        <maven.surefire.version>3.0.0-M5</maven.surefire.version>
        <hamcrest.version>2.2</hamcrest.version>
        <!-- plugins -->
        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
        <slf4j.version>1.7.32</slf4j.version>
        <logback.version>1.2.10</logback.version>
        <!--对应解析-->
        <jackson.version>2.13.1</jackson.version>
        <!--allure报告-->
        <allure.version>2.16.1</allure.version>
        <aspectj.version>1.9.5</aspectj.version>
    </properties>
    <!--    物料清单 (BOM)-->
    <dependencyManagement>
        <dependencies>
            <!--当使用 Gradle 或 Maven 引用多个 JUnit 工件时,此物料清单 POM 可用于简化依赖项管理。不再需要在添加依赖时设置版本-->
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <!--                <version>5.8.2</version>-->
                <version>${junit.jupiter.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <!--            对应添加的依赖的作用范围-->
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>${hamcrest.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <version>${junit.jupiter.version}</version>
        </dependency>
        <!--        yaml文件解析-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!--        csv文件解析-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-csv</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!--        allure报告-->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-junit5</artifactId>
            <version>${allure.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.rest-assured/rest-assured -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>4.5.1</version>
            <scope>compile</scope>
        </dependency>


        <!-- https://mvnrepository.com/artifact/io.qameta.allure/allure-rest-assured -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-rest-assured</artifactId>
            <version>2.16.1</version>
        </dependency>

        <!--json schema -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>4.4.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>${junit.jupiter.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                        <version>${junit.jupiter.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
脚本优化-参数化
使用ParameterizedTest注解,结合ValueSource进行商品名称的参数化
如果是多参数参数化,可以使用 MethodSource 或其他注解。
@ParameterizedTest
@ValueSource(strings = {"hogwarts1", "hogwarts2"})
脚本优化-添加日志(Java)
方式一: 使用REST Assured自带的日志
方式二: 使用 slf4j(配置在下一页)
// REST Assured自带的日志打印
given()
    .log().all() // 请求日志
when()
    .get(url).
then()
    .log().all(); //响应日志
slf4j配置
<!-- 在`resources`中添加配置文件`logback.xml` -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- name指定<appender>的名称    class指定<appender>的全限定名  ConsoleAppender的作用是将日志输出到控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--            输出时间格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}.%M\(%line\) -- %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.ceshiren" level="DEBUG" />
    <logger name="com" level="WARN" />
    <logger name="ceshiren" level="WARN" />
    <logger name="org" level="WARN" />
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
<!-- 注意不要直接粘贴 -->
<!-- properties的版本配置 -->
<slf4j.version>1.7.32</slf4j.version>
<logback.version>1.2.10</logback.version>
<!-- 依赖信息 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${slf4j.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback.version}</version>
    <scope>compile</scope>
</dependency>
脚本优化-数据清理(Java)
在用例执行完成之后调用删除接口完成数据清理
在编写用例的时候,将创建的ID保存到一个数组变量(文件)中,在AfterAll内,调用删除接口批量删除这些数据。
在AfterAll内,先调用获取接口,获取所有的数据信息后调用删除接口删除。
注意: 尽量不要使用AfterEach去做数据清理

脚本优化-报告展示
安装allure相关依赖
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-junit5</artifactId>
    <version>${allure.version}</version>
</dependency>

脚本优化-参数化

  • 使用ParameterizedTest注解,结合ValueSource进行商品名称的参数化
    • 如果是多参数参数化,可以使用 MethodSource 或其他注解。

@ParameterizedTest
@ValueSource(strings = {“hogwarts1”, “hogwarts2”})

脚本优化-添加日志(Java)

  • 方式一: 使用REST Assured自带的日志
  • 方式二: 使用 slf4j(配置在下一页)

// REST Assured自带的日志打印
given()
.log().all() // 请求日志
when()
.get(url).
then()
.log().all(); //响应日志

slf4j配置

<!-- 在`resources`中添加配置文件`logback.xml` -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- name指定<appender>的名称    class指定<appender>的全限定名  ConsoleAppender的作用是将日志输出到控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--            输出时间格式-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36}.%M\(%line\) -- %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.ceshiren" level="DEBUG" />
    <logger name="com" level="WARN" />
    <logger name="ceshiren" level="WARN" />
    <logger name="org" level="WARN" />
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

<!-- 注意不要直接粘贴 -->
<!-- properties的版本配置 -->
<slf4j.version>1.7.32</slf4j.version>
<logback.version>1.2.10</logback.version>
<!-- 依赖信息 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${slf4j.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback.version}</version>
    <scope>compile</scope>
</dependency>

脚本优化-数据清理(Java)

  • 在用例执行完成之后调用删除接口完成数据清理
    • 在编写用例的时候,将创建的ID保存到一个数组变量(文件)中,在AfterAll内,调用删除接口批量删除这些数据。
    • 在AfterAll内,先调用获取接口,获取所有的数据信息后调用删除接口删除。

脚本优化-报告展示

  • 安装allure相关依赖

    io.qameta.allure
    allure-junit5
    ${allure.version}
package com.restassured.litemall;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class LitemallTest {
    static String token;
    static String clientToken;

    static List<Integer> deleteIds = new ArrayList<>();
    static Logger logger = LoggerFactory.getLogger(LitemallTest.class);

    @BeforeAll
    static void setupClass() {
        String loginUrl = "https://litemall.hogwarts.ceshiren.com/admin/auth/login";
        String loginData = "{\"username\":\"hogwarts\",\"password\":\"test12345\",\"code\":\"\"}";
        token = given()
                .body(loginData)
                .contentType("application/json;charset=UTF-8")
                .when()
                .post(loginUrl)
                .then()
//                .log().all();
                .extract().path("data.token");
        System.out.println("token:" + token);

        String clientLoginUrl = "https://litemall.hogwarts.ceshiren.com/wx/auth/login";
        String clientLoginData = "{\"username\":\"user123\",\"password\":\"user123\"}";
        clientToken = given()
                .body(clientLoginData)
                .contentType("application/json;charset=UTF-8")
                .when()
                .post(clientLoginUrl)
                .then()
//                .log().all();
                .extract().path("data.token");
        System.out.println("clientToken:" + clientToken);

    }


    @ParameterizedTest
    @CsvSource({"1234567890,红烧肉", "1234567891,红烧肉2"})
    void addCarts(String goodsSn, String goodsName) {
        logger.info("goodsSn为:{},goodsName为:{}",goodsSn,goodsName);
        //1. 调用上架商品接口,上架商品。
        String addProduct = "https://litemall.hogwarts.ceshiren.com/admin/goods/create";
//        String goodsSn="1234567890";
//        String goodsName="红烧肉";
        String productData = "{\"goods\":{\"picUrl\":\"\",\"gallery\":[],\"isHot\":false,\"isNew\":true,\"isOnSale\":true,\"goodsSn\":\"" + goodsSn + "\",\"name\":\"" + goodsName + "\"},\"specifications\":[{\"specification\":\"规格\",\"value\":\"标准\",\"picUrl\":\"\"}],\"products\":[{\"id\":0,\"specifications\":[\"标准\"],\"price\":\"23\",\"number\":\"100\",\"url\":\"\"}],\"attributes\":[]}";

        given()
                .relaxedHTTPSValidation()
                .header("X-Litemall-Admin-Token", token)
                .body(productData)
                .log().all()
                .contentType("application/json;charset=UTF-8")
                .when()
                .post(addProduct)
                .then().log().all();

        //2. 调用查询商品列表接口,获取商品ID
        String getProductUrl = "https://litemall.hogwarts.ceshiren.com/admin/goods/list";
        Integer goodsId = given()
                .relaxedHTTPSValidation()
                .header("X-Litemall-Admin-Token", token)
                .queryParam("goodsSn", goodsSn)
                .contentType("application/json")
                .when()
                .get(getProductUrl)
                .then()
//                .log().all();
                .extract().path("data.list[0].id");
        System.out.println("goodsId:" + goodsId);

        //3. 查询商品详情接口,获取商品库存ID
        String detailUrl = "https://litemall.hogwarts.ceshiren.com/admin/goods/detail";
        Integer productId = given()
                .header("X-Litemall-Admin-Token", token)
                .param("id", goodsId)
                .contentType("application/json")
                .when()
                .get(detailUrl)
                .then()
                .log().all()
                .extract().path("data.products[0].id");
        System.out.println("productId:" + productId);

        //4. 调用加入购物车接口,添加购物车
        String addCartUrl = "https://litemall.hogwarts.ceshiren.com/wx/cart/add";
        String addCartData = "{\"goodsId\":" + goodsId + ",\"number\":1,\"productId\":" + productId + "}";
        String errmsg = given()
                .header("X-Litemall-Token", clientToken)
                .contentType("application/json")
                .body(addCartData)
                .when()
                .post(addCartUrl)
                .then()
                .log().all()
                .extract().path("errmsg");
        logger.info("添加购物车的断言结果是:{}", errmsg);
        logger.info("goodsId为:{}",goodsId);
        //数据清理:1、获取;2、保存;3、删除
        deleteIds.add(goodsId);
        assertEquals("成功", errmsg);


    }

    @AfterAll
    static void cleanUp() {

        logger.info("获取到的ids信息为:{}", deleteIds);
//        String id = "1444464";
        deleteIds.forEach((id) -> {
            deleteById(id);
        });
    }

    static void deleteById(Integer id) {
        logger.info("id为:{}",id);
        String deleteUrl = "https://litemall.hogwarts.ceshiren.com/admin/goods/delete";
        String deleteData = "{\"id\":" + id + "}";
        String errmsg = given()
                .relaxedHTTPSValidation()
                .header("X-Litemall-Admin-Token", token)
                .contentType("application/json")
                .body(deleteData)
                .when()
                .post(deleteUrl)
                .then()
                .log().all()
                .extract().path("errmsg");
        logger.info("删除商品后的断言结果是:{}", errmsg);
        assertEquals("成功", errmsg);
    }
}