【实战】电子商城接口自动化测试框架实战-高级版

一、框架及知识点梳理

1、框架

1)输入信息(model)

Model复杂程度                        场景                                          使用Model的类型

简单                               1~2个字段                                           参数

正常                               多个字段                                            实体类

非常复杂                       多个字段,且嵌套数据较多                           yaml、json、excel

2)接口信息(api)

3)测试用例(testcase)

2、关键知识点

1)实体类转换为json


a、添加 POM 依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.13.1</version>
</dependency>

b、应用实例:

UserModel userModel = new UserModel();
ObjectMapper mapper = new ObjectMapper();
try {
    String Json =  mapper.writeValueAsString(userModel);
    System.out.println(Json);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

2)Json 快速转换实体类

a、添加IDEA插件
GsonFormatPlus+LomBok

image


b、添加POM依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

3)过滤器

示例代码:

public class DemoFilter implements Filter {
    
    @Override
    public Response filter(FilterableRequestSpecification filterableRequestSpecification,
                           FilterableResponseSpecification filterableResponseSpecification,
                           FilterContext filterContext) {

        // 设置基地址
        filterableRequestSpecification.baseUri("基地址");
        filterableRequestSpecification.contentType("application/json");
        // 发起真实请求
        Response response = filterContext.next(filterableRequestSpecification, filterableResponseSpecification);
        // 拼接请求信息
        String requests_info = filterableRequestSpecification.getMethod() + " "
                + filterableRequestSpecification.getURI()
                + "\n Request Body =>"
                + filterableRequestSpecification.getBody();

        log.debug("接口请求日志:" + requests_info);
        // 在 allure 中添加日志信息
        addAttachment(filterableRequestSpecification.getBasePath() + "接口请求:", requests_info);
        String response_info =
                "\n Response Status => "
                        + response.getStatusCode()
                        + " " + response.getStatusLine()
                        + "\n Response Body => " + response.getBody().asPrettyString();
        addAttachment(filterableRequestSpecification.getBasePath() + "接口响应:", response_info);
        log.debug("接口响应日志:" + response_info);
        return response;
    }
}

4)日志配置

public class Log {
    public static Logger log = LoggerFactory.getLogger("litemall");
}

优雅日志格式-在backlog.xml中添加

<pattern>
<!--优雅的日志格式-->
    %d{dd-MM-yyyy HH:mm:ss.SSS} %highlight(%-5level) %magenta([%thread]) %yellow(%logger{40}.%M\(%class{0}.java:%line\)) - %msg%throwable%n
</pattern>

二、代码实践

1、api包(存放接口类,类中显示接口的各种动作,如增、删、改、查等操作)

1)创建 BaseLitemallApi 类,用于存放接口类中的公共信息,如获取 token ;

package com.ceshiren.hogwarts.litemall.api;

import io.restassured.filter.Filter;
import io.restassured.http.Header;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.restassured.RestAssured.given;

public class BaseLitemallApi {

    public static Logger logger;
    //获取 filter 对象
    // 问题:随着业务接口增多,filter 需要再多处进行实例化操作
    // 解决:在父类进行filter 的实例化操作,子类继承后可以直接调用 filter 实例

    public Filter apiFilter ;

    //Litemall的角色属性,根据角色的不同初始化对应的不同的token信息;
    public String role;
    public Header token;

    public void setRole(String role) {
        this.role = role;
    }
    static void BaseLitemallApi(){
        logger = LoggerFactory.getLogger("litemall");
    }

    public Header getToken() {
        return token;
    }

    public void setToken(Header token) {
        this.token = token;
    }

    public void addRole(BaseLitemallApi baseLitemallApi){

        this.apiFilter = new ApiFilter(baseLitemallApi.getToken());
    }
    //初始化token时应该传入用户名密码
    public void initToken(String username,String password){
        //获取token
        //根据角色的不同初始化对应的不同token信息
        //如果是admin,则初始化admin的token信息
        if(this.role.equals("admin")){
        String paraInfo = "{\"username\":\""+username+"\",\"password\":\""+password+"\",\"code\":\"\"}";
        String tokenValue = given().log().all()
                .contentType("application/json;charset=UTF-8")
                .body(paraInfo)
                .when().post("https://litemall.hogwarts.ceshiren.com/admin/auth/login")
                .then().extract().path("data.token");
        this.token = new Header("X-Litemall-Admin-Token",tokenValue);
            //如果是client,则初始化client的token信息
        } else {

            String info = "{\"username\":\""+username+"\",\"password\":\""+password+"\"}";
            String tokenValue = given().log().all()
                    .contentType("application/json")
                    .body(info)
                    .when().post("https://litemall.hogwarts.ceshiren.com/wx/auth/login")
                    .then().extract().path("data.token");
            this.token = new Header("X-Litemall-Token",tokenValue);
        }
    }
}

2、创建 ApiFilter 类,使用 RestAssured 的 filter 方法对 given() 公共的代码进行整合,减少冗余代码。

package com.ceshiren.hogwarts.litemall.api;

import io.restassured.filter.Filter;
import io.restassured.filter.FilterContext;
import io.restassured.http.Header;
import io.restassured.response.Response;
import io.restassured.specification.FilterableRequestSpecification;
import io.restassured.specification.FilterableResponseSpecification;

import static com.ceshiren.hogwarts.utils.Log.log;
import static io.qameta.allure.Allure.addAttachment;
import static io.restassured.RestAssured.given;
//问题1:如果出现多个角色,多个token应该如何处理
//问题2:多次调用login请求获取token,影响用例的执行效率;
//解决方案:添加一个初始化token的方法,并调出,并添加一个初始化filter的方法,传入token给filter;
public class ApiFilter implements Filter {
    public Header tokenHeader;
    public  ApiFilter(Header token){
        this.tokenHeader = token;
    }
    @Override
    public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext context) {

        //添加基地址
        requestSpec.baseUri("https://litemall.hogwarts.ceshiren.com/");
        //添加头部信息-token
        requestSpec.contentType("application/json");
        requestSpec.header(tokenHeader);
        //3、日志配置:即每个接口在调用过程中,自动调用日志信息
        //请求信息
        String requests_info = "\n 请求方法:" + requestSpec.getMethod()
                +"\n 请求地址: "+requestSpec.getURI()
                +"\n 请求体:"
                + requestSpec.getBody();
        //真实发起请求
        Response response =  context.next(requestSpec,responseSpec);
        //响应信息
        String response_info = "\n 响应状态:"
                + response.getStatusCode()
                + "\n 响应行: " + response.getStatusLine()
                + "\n 响应体:" + response.getBody().asPrettyString();


        log.debug("接口请求" + requests_info);
        addAttachment("接口请求" ,requests_info);
        log.debug("接口响应" + response_info);
        addAttachment("接口响应" , response_info);
        return response;
    }
}

3、以 商品接口 (GoodsApi)中包含接口的增、删、改、查;

package com.ceshiren.hogwarts.litemall.api;

import com.ceshiren.hogwarts.litemall.model.CartModel;
import com.ceshiren.hogwarts.litemall.model.GoodsModel;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.filter.Filter;
import io.restassured.response.Response;

import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.requestSpecification;

//业务模板:货品相关
public class GoodsApi extends BaseLitemallApi {

        //增删查改

    //问题一:如果传入的数据格式必须是json,如果把object转换为json(选修)
    //解决方案:使用jack直播转换即可
//            ObjectMapper mapper = new ObjectMapper();
//            try {
//                System.out.println(mapper.writeValueAsString(carModel));
//            } catch (JsonProcessingException e) {
//                e.printStackTrace();
//            }
        public Response create(String goodsData){

            return   given().filter(apiFilter)
                    .body(goodsData)
                    .when().post("/admin/goods/create");
        }

        public void delete(Integer id){
            String body  = "{\"id\": \"" + id + "\"}";
            given().filter(apiFilter)
                    .body(body)
                    .when().post("/admin/goods/delete")
                    .then();
        }

        public String get(String goodsSn){
            String goodIdsObject = given().filter(apiFilter)
                    .param("goodsSn",goodsSn)
                    .when().get("/admin/goods/list")
                    .then().extract().body().asString();

            return goodIdsObject;

        }

        public void update(){

        }

        public String getDetail(Integer goodIds){
            String productObject = given().filter(apiFilter)
//                    .header("X-Litemall-Admin-Token",token)
                    .param("id",goodIds)
                    .when().get("/admin/goods/detail")
                    .then().extract().body().asString();

            return productObject;

        }



    }

2、model包(存放接口类,类中主要包含接口字段或get、set方法)

1)GoodsModel 类(存入商品接口字段及get\set方法)

package com.ceshiren.hogwarts.litemall.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Data
public class GoodsModel {

    @JsonProperty("goods")
    private GoodsDTO goods;

    @NoArgsConstructor
    @Data
    public static class GoodsDTO {
        @JsonProperty("goodsSn")
        private String goodsSn;
        @JsonProperty("name")
        private String name;
    }
}

2)CartModel类(存入购物车接口字段及get\set方法)

package com.ceshiren.hogwarts.litemall.model;

public class CartModel {
    private  Integer goodsId;
    private Integer number;
    private Integer productId;

    public Integer getGoodsId() {
        return goodsId;
    }

    public void setGoodsId(Integer goodsId) {
        this.goodsId = goodsId;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    public Integer getProductId() {
        return productId;
    }

    public void setProductId(Integer productId) {
        this.productId = productId;
    }



}

3、通用包(存入通用工具类,如 log)

package com.ceshiren.hogwarts.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Log {

    public static Logger log = LoggerFactory.getLogger("litemall");
}

4、测试包(存放测试用例)

1)创建 LitemallBaseTest 类,存放一些公共代码

package com.ceshiren.hogwarts.litemall.api;

import org.junit.jupiter.api.BeforeAll;

public class LitemallBaseTest {
    static BaseLitemallApi admin;
    static BaseLitemallApi client;
    @BeforeAll
    static void setUp(){
        //问题:market 和 goods 都属于同一个角色。使用的功能,在这个过程中,不需要切换角色。
        //解决方案:角色的初使化不要和具体的业务挂钩,将角色的初始化和业务的初始化分开进行。
        //实例化一个 admin 角色
        admin = new BaseLitemallApi();
        admin.setRole("admin");
        admin.initToken("admin123","admin123");
        //实例化一个客户角色
        client = new BaseLitemallApi();
        client.setRole("client");
        client.initToken("user123","user123");
    }
}

2)创建 CartApiTest 类,实现
//步骤1:调用上架商品接口,上架商品
//步骤2:调用查询商品列表接口,获取商品ID
//步骤3:查询商品详情接口,获取商品库存ID
//步骤4:调用添加购物车接口

package com.ceshiren.hogwarts.litemall.api;

import com.ceshiren.hogwarts.litemall.api.ApiFilter;
import com.ceshiren.hogwarts.litemall.api.BaseLitemallApi;
import com.ceshiren.hogwarts.litemall.api.CartApi;
import com.ceshiren.hogwarts.litemall.api.GoodsApi;
import com.ceshiren.hogwarts.litemall.model.CartModel;
import com.jayway.jsonpath.JsonPath;
import io.restassured.filter.Filter;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.slf4j.Logger;
import static com.ceshiren.hogwarts.utils.Log.log;
import java.util.ArrayList;
import java.util.List;

class CartApiTest extends LitemallBaseTest{

    static GoodsApi goodsApi;
    static CartApi cartApi;
    static List<Integer> deleteList = new ArrayList<>();


    @BeforeAll
    static void setUpClass(){
        goodsApi = new GoodsApi();
        goodsApi.addRole(admin);
        cartApi = new CartApi();
        cartApi.addRole(client);
        }
    @AfterAll
    static void cleanUp(){
        //删除
        deleteList.forEach((id) -> {
            log.info("删除的ID为:",id);
            goodsApi.delete(id);
        });

    }

    //问题二:token 依然暴露在测试用例里面,且token和cart没有任何的业务关系,应该隐藏起来
    //问题三:输入以及输出
    @ParameterizedTest
    @CsvSource({"S180009,小熊09"})
    void create(String goodsSn,String name) {
        //步骤1:调用上架商品接口,上架商品
        String goodsData = "{\"goods\":{\"picUrl\":\"\",\"gallery\":[],\"isHot\":false,\"isNew\":true,\"isOnSale\":true,\"goodsSn\":\""+goodsSn+"\",\"name\":\""+name+"\"},\"specifications\":[{\"specification\":\"规格\",\"value\":\"标准\",\"picUrl\":\"\"}],\"products\":[{\"id\":0,\"specifications\":[\"标准\"],\"price\":0,\"number\":0,\"url\":\"\"}],\"attributes\":[]}";
        goodsApi.create(goodsData).then().statusCode(200);
        CartModel cartModel = new CartModel();
        //步骤2:调用查询商品列表接口,获取商品ID

        //体现出接口之间的数据关联
        String goodsObject = goodsApi.get(goodsSn);
        System.out.println("goodsObject的值为"+goodsObject);
        List<Integer> goodsid = JsonPath.read(goodsObject,"$..id");
        System.out.println("List<Integer> goodsid的值为:"+ goodsid.toString());


        cartModel.setGoodsId(goodsid.get(0));

        //步骤3:查询商品详情接口,获取商品库存ID
        //.path("data.products[0].id")
        String productObject = goodsApi.getDetail(goodsid.get(0));
        List<Integer> productId = JsonPath.read(productObject,"$..products[0].id");
        cartModel.setProductId(productId.get(0));
        cartModel.setNumber(1);

        //步骤4:调用添加购物车接口
        cartApi.create(cartModel);
        deleteList.add(goodsid.get(0));

    }
}

三、pom 相关依赖

<?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>org.example</groupId>
    <artifactId>hogwarts-api</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
       <!--<module>hogwarts-api</module>-->
    </modules>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <!--Junit5-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.0-M1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.0-M1</version>
            <scope>test</scope>
        </dependency>
        <!--RestAssured-->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>5.1.1</version>
        </dependency>


        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>

        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
            <version>2.7.0</version>
        </dependency>

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>5.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.13.4</version>
        </dependency>

         <!--日志-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.3</version>
        </dependency>
         <!--allure-->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-junit5</artifactId>
            <version>2.16.0</version>
        </dependency>



    </dependencies>

</project>

四、logback相关代码

<configuration>
    <!-- name指定<appender>的名称    class指定<appender>的全限定名  ConsoleAppender的作用是将日志输出到控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--            输出时间格式-->
            <pattern>
                <!--优雅的日志格式-->
                %d{dd-MM-yyyy HH:mm:ss.SSS} %highlight(%-5level) %magenta([%thread]) %yellow(%logger{40}.%M\(%class{0}.java:%line\)) - %msg%throwable%n
            </pattern>
        </encoder>
    </appender>


    <logger name="litemall" level="DEBUG" />
    <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>