Skip to content

【实战】宠物商店接口自动化测试实战

【实战】宠物商店接口自动化测试实战

被测产品

  • PetStore 宠物商城:
    • 一个在线的小型的商城。
    • 主要提供了增删查改等操作接口。
    • 结合 Swagger 实现了接口的管理。

需求说明

  • 完成宠物商城宠物管理功能接口自动化测试。
    • 编写自动化测试脚本。
    • 完成复杂断言。

相关知识点

形式 章节 描述
知识点 代理配置 利用代理分析测试脚本,排查请求错误
知识点 多层嵌套响应断言 利用 jsonpath 进行多层嵌套的响应断言

实战思路

uml diagram

需求分析

  • 被测产品:宠物商店系统 - 宠物管理。
  • 宠物商店接口文档:https://petstore.swagger.io/
  • 宠物管理业务场景:
    • 添加宠物。
    • 查询宠物信息。
    • 修改宠物信息。
    • 删除宠物。

uml diagram

宠物管理接口业务流程测试用例

编写自动化测试脚本思路

uml diagram

编写自动化测试脚本

Python 代码示例:

class TestPetstorePetmanager:

    def setup_class(self):
        # 准备测试数据
        self.base_url = "https://petstore.swagger.io/v2/pet"
        self.search_url = self.base_url + "/findByStatus"
        self.pet_id = 9223372000001084222
        pet_status = "available"
        self.pet_info = {
            "id": self.pet_id,
            "category": {
                "id": 1,
                "name": "cat"
            },
            "name": "miao",
            "photoUrls": [
                "string"
            ],
            "tags": [
                {
                    "id": 5,
                    "name": "cute"
                }
            ],
            "status": pet_status
        }
        self.search_param = {
            "status": pet_status
        }
        self.update_name = "miao-hogwarts"
        self.update_info = {
            "id": self.pet_id,
            "category": {
                "id": 1,
                "name": "cat"
            },
            "name": self.update_name,
            "photoUrls": [
                "string"
            ],
            "tags": [
                {
                    "id": 5,
                    "name": "cute"
                }
            ],
            "status": pet_status
        }
        self.delete_url = self.base_url + f"/{self.pet_id}"

    def test_pet_manager(self):

        # 新增宠物
        add_r = requests.post(self.base_url, json=self.pet_info)
        # 查询宠物
        search_r = requests.get(self.search_url, params=self.search_param)
        # 修改宠物
        update_r = requests.put(self.base_url, json=self.update_info)
        # 删除宠物
        delete_r =requests.delete(self.delete_url)

Java 代码示例:

/*
 * @Author: 霍格沃兹测试开发学社
 * @Desc: '更多测试开发技术探讨,请访问:https://ceshiren.com/t/topic/15860'
 */


import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.restassured.http.ContentType;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.List;

import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertAll;

public class PetTest {
    static String baseURL;
    static String petId;
    static String newPetName;
    static String updatePetName;

    @BeforeAll
    public static void beforeAll() {
        baseURL = "https://petstore.swagger.io/v2/pet";
        petId = "1234567891234567";
        newPetName = "newPet";
        updatePetName = "updatePet";
    }

    //添加宠物测试用例
    @Test
    @Order(66)
    public void addTest() {
        add();
        String body = get();
        DocumentContext context = JsonPath.parse(body);
        List<Long> ids = context.read("$..id");//$[*].id
        List<String> names = context.read("$..name");//$[*].name
        List<String> status = context.read("$..status");//$[*].status

        System.out.println(status);
        assertAll(
                () -> assertThat(status, everyItem(equalTo("available"))),
                () -> assertThat(ids, Matchers.hasItem(Long.valueOf(petId))),
                () -> assertThat(names, Matchers.hasItem(newPetName))
        );
    }

    @Order(122)
    @Test
    public void updateTest() {
        update();
        String body = get();
        DocumentContext context = JsonPath.parse(body);
        List<Long> ids = context.read("$..id");
        List<String> names = context.read("$..name");
        assertAll(
                () -> assertThat(ids, Matchers.hasItem(Long.valueOf(petId))),
                () -> assertThat(names, Matchers.hasItem(updatePetName))
        );
    }

    @Test
    @Order(300)
    public void deleteTest() {
        delete();
        String body = get();
        DocumentContext context = JsonPath.parse(body);
        List<Long> ids = context.read("$..id");
        assertAll(
                () -> assertThat(ids, not(Matchers.hasItem(Long.valueOf(petId))))
        );
    }


    //新增 POST https://petstore.swagger.io/v2/pet
    public void add() {
        String newPet = "{\"id\":" + petId + ",\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"" + newPetName + "\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"}";
        given()
                .log().all()
                .contentType(ContentType.JSON)
                .body(newPet)
                .when()
                .post(baseURL)
                .then().log().all();

    }

    public String get() {
        HashMap<String, Object> statusQuery = new HashMap<>() {
            put("status", "available");
        };
        String body = given().log().all()
                .queryParams(statusQuery)
                .when()
                .get(baseURL + "/findByStatus")
                .then().log().all()
                .extract().response()
                .getBody().asString();
        System.out.println(body);
        return body;

    }

    //更新 PUT https://petstore.swagger.io/v2/pet
    public void update() {
        String updatePet = "{\"id\":" + petId + ",\"category\":{\"id\":0,\"name\":\"string\"},\"name\":\"" + updatePetName + "\",\"photoUrls\":[\"string\"],\"tags\":[{\"id\":0,\"name\":\"string\"}],\"status\":\"available\"}";
        given().log().all()
                .contentType(ContentType.JSON)
                .body(updatePet)
                .when()
                .put(baseURL)
                .then().log().all();

    }

    //删除 DELETE  https://petstore.swagger.io/v2/pet petId
    public void delete() {
        given().log().all()
                .when()
                .delete(baseURL + "/" + petId)
                .then().log().all();

    }
}

脚本优化 - 配置代理查看接口数据

  • 在脚本中配置代理。
  • 抓包查看接口测试中的接口请求和响应数据。
class TestPetclinicPetmanager:

    def setup_class(self):

        # 配置代理
        self.proxy = {
            "http": "http://127.0.0.1:8888",
            "https": "http://127.0.0.1:8888"
        }
    def test_pet_manager(self):

        # 新增宠物
        add_r = requests.post(self.base_url, json=self.pet_info, proxies=self.proxy, verify=False)
        # 查询新增宠物结果
        search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
        # 修改宠物
        update_r = requests.put(self.base_url, json=self.update_info, proxies=self.proxy, verify=False)
        # 删除宠物
        delete_r =requests.delete(self.delete_url, proxies=self.proxy, verify=False)

脚本优化 - 添加日志

  • 新建日志配置。
  • 在用例中使用配置好的日志实例。
# 配置日志
import logging
import os

from logging.handlers import RotatingFileHandler

# 绑定绑定句柄到logger对象
logger = logging.getLogger(__name__)
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
log_dir_path = os.sep.join([root_path, '..', f'/logs'])
if not os.path.isdir(log_dir_path):
    os.mkdir(log_dir_path)
# 创建日志记录器,指明日志保存路径,每个日志的大小,保存日志的上限
file_log_handler = RotatingFileHandler(os.sep.join([log_dir_path, 'log.log']), maxBytes=1024 * 1024, backupCount=10)
# 设置日志的格式
date_string = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(
    '[%(asctime)s] [%(levelname)s] [%(filename)s]/[line: %(lineno)d]/[%(funcName)s] %(message)s ', date_string)
# 日志输出到控制台的句柄
stream_handler = logging.StreamHandler()
# 将日志记录器指定日志的格式
file_log_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# 为全局的日志工具对象添加日志记录器
# 绑定绑定句柄到logger对象
logger.addHandler(stream_handler)
logger.addHandler(file_log_handler)
# 设置日志输出级别
logger.setLevel(level=logging.INFO)

在添加日志过程中,拼接业务步骤

class TestPetclinicPetmanager:

    def setup_class(self):
        ...

    def test_pet_manager(self):

        # 新增宠物
        add_r = requests.post(self.base_url, json=self.pet_info, proxies=self.proxy, verify=False)
        logger.info(f"新增宠物接口响应为:{add_r.text}")
        # 查询宠物
        search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
        logger.info(f"查询接口响应为:{search_r.text}")
        # 修改宠物
        update_r = requests.put(self.base_url, json=self.update_info, proxies=self.proxy, verify=False)
        logger.info(f"更新接口响应为:{update_r.text}")
        # 删除宠物
        delete_r =requests.delete(self.delete_url, proxies=self.proxy, verify=False)
        logger.info(f"删除接口响应为:{delete_r.text}")

脚本优化 - 使用 jsonpath 断言

  • 使用 jsonpath 实现多层嵌套响应的断言。
jsonpath.jsonpath(r.json(), "$..id")

拼接业务场景,完成断言。

class TestPetclinicPetmanager:

    def setup_class(self):
        ...

    def test_pet_manager(self):

        # 新增宠物
        add_r = requests.post(self.base_url, json=self.pet_info, proxies=self.proxy, verify=False)
        logger.info(f"新增宠物接口响应为:{add_r.text}")
        # 状态断言
        assert add_r.status_code == 200
        # 查询新增宠物结果
        search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
        logger.info(f"查询接口响应为:{search_r.text}")
        # 状态断言
        assert search_r.status_code == 200
        # 业务断言
        assert self.pet_id in jsonpath.jsonpath(search_r.json(), "$..id")
        # 修改宠物
        update_r = requests.put(self.base_url, json=self.update_info, proxies=self.proxy, verify=False)
        logger.info(f"更新接口响应为:{update_r.text}")
        # 状态断言
        assert update_r.status_code == 200
        # 查询更新宠物结果
        search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
        logger.info(f"查询接口响应为:{search_r.text}")
        # 状态断言
        assert search_r.status_code == 200
        # 业务断言
        assert self.update_name in jsonpath.jsonpath(search_r.json(), "$..name")
        # 删除宠物
        delete_r =requests.delete(self.delete_url, proxies=self.proxy, verify=False)
        logger.info(f"删除接口响应为:{delete_r.text}")
        # 状态断言
        assert delete_r.status_code == 200
        # 查询删除结果
        search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
        logger.info(f"查询接口响应为:{search_r.text}")
        # 状态断言
        assert search_r.status_code == 200
        # 业务断言
        assert self.pet_id not in jsonpath.jsonpath(search_r.json(), "$..id")

生成测试报告

  • 安装 allure 相关依赖。

先给测试脚本填写一些描述信息。

@allure.feature("宠物管理业务场景测试")
class TestPetclinicPetmanager:

    def setup_class(self):
        ...

    @allure.story("宠物增删改查场景测试")
    def test_pet_manager(self):

        with allure.step("新增宠物"):
            # 新增宠物
            add_r = requests.post(self.base_url, json=self.pet_info, proxies=self.proxy, verify=False)
            logger.info(f"新增宠物接口响应为:{add_r.text}")
            # 状态断言
            assert add_r.status_code == 200
            # 查询新增宠物结果
            search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
            logger.info(f"查询接口响应为:{search_r.text}")
            assert self.pet_id in jsonpath.jsonpath(search_r.json(), "$..id")
        with allure.step("修改宠物信息"):
            # 修改宠物
            update_r = requests.put(self.base_url, json=self.update_info, proxies=self.proxy, verify=False)
            logger.info(f"更新接口响应为:{update_r.text}")
            # 状态断言
            assert update_r.status_code == 200
            # 查询更新宠物结果
            search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
            logger.info(f"查询接口响应为:{search_r.text}")
            assert self.update_name in jsonpath.jsonpath(search_r.json(), "$..name")
        with allure.step("删除宠物"):
            # 删除宠物
            delete_r =requests.delete(self.delete_url, proxies=self.proxy, verify=False)
            logger.info(f"删除接口响应为:{delete_r.text}")
            # 状态断言
            assert delete_r.status_code == 200
            # 查询删除结果
            search_r = requests.get(self.search_url, params=self.search_param, proxies=self.proxy, verify=False)
            logger.info(f"查询接口响应为:{search_r.text}")
            assert self.pet_id not in jsonpath.jsonpath(search_r.json(), "$..id")

执行这个测试脚本

# 生成报告信息
pytest -vs test_petclinic_petmanager.py --alluredir=./report --clean-alluredir
# 生成报告在线服务,查看报告
allure serve ./report/

总结

  • 通过 Swagger 文档获取接口信息。
  • 使用 Requests 发出请求。
  • 添加代理,抓包查看接口请求和响应数据。
  • 使用 Jsonpath 提取复杂结构响应数据,然后进行断言。
  • 添加 Log 日志。
  • 生成 Allure 测试报告。