pytestx容器化执行引擎

news2024/9/25 19:14:00

系统架构

前端、后端、pytest均以Docker容器运行服务,单独的容器化执行引擎,项目环境隔离,即用即取,用完即齐,简单,高效。

  • 前端容器:页面交互,请求后端,展示HTML报告

  • 后端容器:接收前端请求,启动任务,构建镜像,触发运行pytest,挂载HTML报告

  • pytest容器:拉取项目代码,指定目录执行,生成HTML报告

说明:构建镜像目前是在宿主机启动后端服务来执行docker命令的,暂未支持Kubernetes编排。宿主机安装了Docker,启动服务后,可以执行docker命令。如果采用容器部署后端,容器里面不包含Docker,无法构建,个人想法是可以借助K8S来编排,当前版本还未实现

系统流程

支持2种运行模式配置:容器和本地。

容器模式:判断是否支持docker,如果支持,构建pytest镜像,在构建时,通过git拉取项目代码,再运行容器,按照指定目录执行pytest,生成测试报告,并将报告文件挂载到后端。如果不支持,降级为本地运行。

本地模式:模拟容器行为,在本地目录拉取代码,执行pytest,生成测试报告。

效果展示

任务管理:

容器模式:

本地模式:

平台大改造

pytestx平台更轻、更薄,移除了用例管理、任务关联用例相关功能代码,只保留真正的任务调度功能,backend的requirements.txt解耦,只保留后端依赖,pytest相关依赖转移到tep-project。

那如何管理用例呢?约定大于配置,我们约定pytest项目已经通过目录维护好了一个稳定的自动化用例集,也就是说需要通过平台任务调度的用例,都统一存放在目录X下,这些用例基本不需要维护,可以每日稳定执行,然后将目录X配置到平台任务信息中,按指定目录执行用例集。对于那些不够稳定的用例,就不能放到目录X下,需要调试好以后再纳入。

为什么不用marker?pytest的marker确实可以给测试用例打标记,也有人是手动建立任务和用例进行映射,这些方式都不如维护一个稳定的自动化用例集方便,在我们公司平台上,也是维护用例集,作为基础用例集。使用pytest项目同理。

核心代码

一键部署

#!/bin/bash
PkgName='backend'

Dockerfile='./deploy/Dockerfile.backend'
DockerContext=./

echo "Start build image..."
docker build -f $Dockerfile -t $PkgName $DockerContext
if [ $? -eq 0 ]
then
    echo "Build docker image success"
    echo "Start run image..."
    docker run -p 8000:80 $PkgName
else
    echo "Build docker image failed"
fi
FROM python:3.8

ENV LANG C.UTF-8
ENV TZ=Asia/Shanghai

RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

WORKDIR /app
COPY ./backend .
RUN pip install -r ./requirements.txt -i \
    https://pypi.tuna.tsinghua.edu.cn/simple \
    --default-timeout=3000

CMD ["python", "./manage.py", "runserver", "0.0.0.0:80"]

数据库表

更精简,只有project和task两张表,简化平台功能,聚焦任务调度:

需要说明的是,如果多人运行任务,只会存储最后一次执行结果,这个问题不是核心,个人精力有限,不打算在开源项目中开发,更侧重于实现任务调度,供大家参考

执行任务

settings配置任务模式,判断执行不同分支:

def run(self):
    logger.info("任务开始执行")
    if settings.TASK_RUN_MODE == TaskRunMode.DOCKER:  # 容器模式
        try:
            self.execute_by_docker()
        except Exception as e:
            logger.info(e)
            if e == TaskException.DockerNotSupportedException:
                logger.info("降级为本地执行")
                self.execute_by_local()
    if settings.TASK_RUN_MODE == TaskRunMode.LOCAL:  # 本地模式
        self.execute_by_local()
    self.save_task()

容器模式

先根据docker -v命令判断是否支持docker,然后docker build,再docker run

def execute_by_docker(self):
    logger.info("运行模式:容器")
    output = subprocess.getoutput("docker -v")
    logger.info(output)
    if "not found" in output:
        raise TaskException.DockerNotSupportedException
    build_args = [
        f'--build-arg CMD_GIT_CLONE="{self.cmd_git_clone}"',
        f'--build-arg GIT_NAME="{self.git_name}"',
        f'--build-arg EXEC_DIR="{self.exec_dir}"',
        f'--build-arg REPORT_NAME="{self.report_name}"',
    ]
    cmd = f"docker build {' '.join(build_args)} -f {self.dockerfile_pytest} -t {self.git_name} {BASE_DIR}"
    logger.info(cmd)
    output = subprocess.getoutput(cmd)
    logger.info(output)
    cmd = f"docker run -v {REPORT_PATH}:/app/{os.path.join(self.exec_dir, 'reports')} {self.git_name}"
    logger.info(cmd)
    output = subprocess.getoutput(cmd)
    logger.info(output)

将项目仓库、执行目录、报告名称信息,通过参数传入Dockerfile.pytest

FROM python:3.8

ENV LANG C.UTF-8
ENV TZ=Asia/Shanghai
ARG CMD_GIT_CLONE
ARG GIT_NAME
ARG EXEC_DIR
ARG REPORT_NAME

RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

WORKDIR /app
RUN $CMD_GIT_CLONE
RUN pip install -r ./$GIT_NAME/requirements.txt -i \
    https://pypi.tuna.tsinghua.edu.cn/simple \
    --default-timeout=3000

WORKDIR $EXEC_DIR
ENV HTML_NAME=$REPORT_NAME
CMD ["pytest", "--html=./reports/$HTML_NAME", "--self-contained-html"]

docker run的-v参数将容器报告挂载在后端服务,当报告生成后,后端服务也会生成一份报告文件。再将文件内容返给前端展示:

def report(request, *args, **kwargs):
    task_id = kwargs["task_id"]
    task = Task.objects.get(id=task_id)
    report_path = task.report_path

    with open(os.path.join(REPORT_PATH, report_path), 'r', encoding="utf8") as f:
        html_content = f.read()

    return HttpResponse(html_content, content_type='text/html')

测试报告使用的pytest-html,重数据内容,轻外观样式。

本地模式

模拟容器行为,把本地.local目录当做容器,拉代码,执行pytest,生成报告,复制报告到报告文件夹,删除本地目录:

def execute_by_local(self):
    logger.info("运行模式:本地")
    os.makedirs(self.local_path, exist_ok=True)
    os.chdir(self.local_path)
    cmd_list = [self.cmd_git_clone, self.cmd_pytest]
    for cmd in cmd_list:
        logger.info(cmd)
        output = subprocess.getoutput(cmd)
        if output:
            logger.info(output)
    os.makedirs(REPORT_PATH, exist_ok=True)
    shutil.copy2(self.project_report_path, REPORT_PATH)
    shutil.rmtree(LOCAL_PATH)

本地模式,主要用于本地调试,在缺失Docker环境时,也能调试其他功能。

配置

TASK_RUN_MODE = TaskRunMode.DOCKER
LOCAL_PATH = os.path.join(BASE_DIR, ".local")
REPORT_PATH = os.path.join(BASE_DIR, "task", "report")
class TaskRunner:
    def __init__(self, task_id, run_user_id):
        self.task_id = task_id
        self.directory = Task.objects.get(id=task_id).directory
        self.project_id = Task.objects.get(id=task_id).project_id
        self.git_repository = Project.objects.get(id=self.project_id).git_repository
        self.git_branch = Project.objects.get(id=self.project_id).git_branch
        self.git_name = re.findall(r"^.*/(.*).git", self.git_repository)[0]
        self.local_path = os.path.join(LOCAL_PATH, str(uuid.uuid1()).replace("-", ""))
        self.run_user_id = run_user_id
        self.current_time = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(time.time()))
        self.report_name = f"{str(self.git_name)}-{self.task_id}-{self.run_user_id}-{self.current_time}.html"
        self.project_report_path = os.path.join(self.local_path, self.git_name, "reports", self.report_name)
        self.dockerfile_pytest = os.path.join(DEPLOY_PATH, "Dockerfile.pytest")
        self.exec_dir = os.path.join(self.git_name, self.directory)
        self.cmd_git_clone = f"git clone -b {self.git_branch} {self.git_repository}"
        self.cmd_pytest = f"pytest {self.exec_dir} --html={self.project_report_path} --self-contained-html"

tep-project更新

1、整合fixture,功能类放在fixture_function模块,数据类放在其他模块,突出fixture存放数据概念,比如登录接口fixture_login存储用户名密码、数据库fixture_mysql存储连接信息、文件fixture_file_data存储文件路径

2、改造fixture_login,数据类fixture代码更简洁

import pytest
from loguru import logger


@pytest.fixture(scope="session")
def login(http, file_data):
    logger.info("----------------开始登录----------------")
    response = http(
        "post",
        url=file_data["domain"] + "/api/users/login",
        headers={"Content-Type": "application/json"},
        json={"username": "admin", "password": "qa123456"}
    )
    assert response.status_code < 400
    logger.info("----------------登录成功----------------")
    response = response.json()
    return {"Content-Type": "application/json", "Authorization": f"Bearer {response['token']}"}


@pytest.fixture(scope="session")
def login_xdist(http, tep_context_manager, file_data):
    """
    该login只会在整个运行期间执行一次
    """

    def produce_expensive_data(variable):
        logger.info("----------------开始登录----------------")
        response = http(
            "post",
            url=variable["domain"] + "/api/users/login",
            headers={"Content-Type": "application/json"},
            json={"username": "admin", "password": "qa123456"}
        )
        assert response.status_code < 400
        logger.info("----------------登录成功----------------")
        return response.json()

    response = tep_context_manager(produce_expensive_data, file_data)
    return {"Authorization": "Bearer " + response["token"]}

3、改造fixture_mysql,支持维护多个连接,并且保持简洁

fixture_function.py

@pytest.fixture(scope="class")
def executor():
    class Executor:
        def __init__(self, db):
            self.db = db
            self.cursor = db.cursor()

        def execute_sql(self, sql):
            try:
                self.cursor.execute(sql)
                self.db.commit()
            except Exception as e:
                print(e)
                self.db.rollback()
            return self.cursor

    return Executor

fixture_mysql.py

@pytest.fixture(scope="class")
def mysql_execute(executor):
    db = pymysql.connect(host="host",
                         port=3306,
                         user="root",
                         password="password",
                         database="database")
    yield executor(db).execute_sql
    db.close()


@pytest.fixture(scope="class")
def mysql_execute_x(executor):
    db = pymysql.connect(host="x",
                         port=3306,
                         user="x",
                         password="x",
                         database="x")
    yield executor(db).execute_sql
    db.close()

4、改造fixture_file_data,并添加示例test_file_data.py

import os

import pytest

from conftest import RESOURCE_PATH


@pytest.fixture(scope="session")
def file_data(resource):
    file_path = os.path.join(RESOURCE_PATH, "demo.yaml")
    return resource(file_path).get_data()


@pytest.fixture(scope="session")
def file_data_json(resource):
    file_path = os.path.join(RESOURCE_PATH, "demo.json")
    return resource(file_path).get_data()

5、添加接口复用的示例代码

tests/base就是平台调度使用的稳定自动化用例集。

接口代码复用设计

5条用例:

  1. test_search_sku.py:搜索商品,前置条件:登录

  2. test_add_cart.py:添加购物车,前置条件:登录,搜索商品

  3. test_order.py:下单,前置条件:登录,搜索商品,添加购物车

  4. test_pay.py:支付,前置条件:登录,搜索商品,添加购物车,下单

  5. test_flow.py:完整流程

怎么设计?

  • 登录,每条用例前置条件都依赖,定义为fixture_login,放在fixtures目录下

  • 搜索商品,test_search_sku.py用例本身不需要复用,被前置条件依赖3次,可以复用

①定义为fixture_search_sku放在fixtures❌ 弊端:导致fixtures臃肿

②复制用例文件,允许多份代码,平行展开✅ 好处:高度解耦,不用担心依赖问题

总结,定义为fixture需要具备底层性,足够精炼。对于业务接口用例的前置条件,尽量在用例文件内部处理,保持文件解耦,遵循独立可运行的原则。

复制多份文件?需要修改的话要改多份文件?

是的,但这种情况极少。我能想到的情况:一、框架设计不成熟,动了底层设计,二、接口不稳定,改了公共接口,三、用例设计不合理,不能算是自动化。接口自动化要做好的前提,其实就是框架成熟,接口稳定,用例设计合理,满足这些前提以后,沉淀下来的自动化用例,几乎不需要大批量修改,更多的是要针对每条用例,去修改内部的数据,以满足不同场景的测试需要。也就是说,针对某个用例修改这个用例的数据,是更常见的行为。

如果项目变动实在太大,整个自动化都不能用了,不管是做封装还是平行展开,维护量都非常大,耦合度太高的话,反而还不好改。

跟着pytestx学习接口自动化框架设计,更简单,更快速,更高效

https://github.com/dongfanger/pytestx

https://gitee.com/dongfanger/tep-project

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/930165.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023-8-25食物链

题目链接&#xff1a;食物链 #include <iostream>using namespace std;const int N 50010;int n, m; int p[N], d[N];int find(int x) {if(p[x] ! x){int t find(p[x]);d[x] d[p[x]];p[x] t;}return p[x]; }int main() {cin >> n >> m;for(int i 1; i…

Could not initialize class net.sf.cglib.beans.BeanMap$Generator异常解决

目录 前言 解决方法 复盘分析 前言 使用easyexcel时&#xff0c;研发环境正常, 服务器编译正常, EasyExcel调用方法的时候, 服务器出现Could not initialize class net.sf.cglib.beans.BeanMap$Generator问题。 报错信息 com.alibaba.excel.exception.ExcelAnalysisExc…

​LeetCode解法汇总2236. 判断根结点是否等于子结点之和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个 …

帆软报表系统查看数据库密码

有子曰&#xff1a;“礼之用&#xff0c;和为贵。先王之道&#xff0c;斯为美。小大由之。有所不行&#xff0c;知和而和&#xff0c;不以礼节之&#xff0c;亦不可行也。” 查看数据库密码 构造payload&#xff0c;访问漏洞url&#xff1a; /ReportServer?opfr_server&…

商品搜索网:连接您与各类商品的桥梁

导语&#xff1a;在如今信息爆炸的时代&#xff0c;购物已经不再是传统的实体店购买&#xff0c;而是通过互联网实现的线上购物方式。而要实现高效的线上购物&#xff0c;商品搜索引擎则成为我们的得力助手。作为国内垂直的商品搜索之一&#xff0c;为中国用户提供全面的数码电…

创建abp vnext项目

需求&#xff1a; 1.使用net core跨平台的方式支持windows和centos系统&#xff1b; 2.实现前后端分离部署 3.框架默认集成用户登录、权限、redis等模块 4.支持多种数据库的方式 5.前端使用vue&#xff0c;不需要使用框架自带的web 1.框架配置官网地址&#xff1a; https://ab…

改进YOLO系列:8.添加SimAM注意力机制

添加SimAM注意力机制 1. SimAM注意力机制论文2. SimAM注意力机制原理3. SimAM注意力机制的配置3.1common.py配置3.2yolo.py配置3.3yaml文件配置1. SimAM注意力机制论文 论文题目:SimAM: A Simple, Parameter-Free Attention Module for Convolutional Neural Network…

Kaggle(3):Predict CO2 Emissions in Rwanda

Kaggle&#xff08;3&#xff09;&#xff1a;Predict CO2 Emissions in Rwanda 1. Introduction 在本次竞赛中&#xff0c;我们的任务是预测非洲 497 个不同地点 2022 年的二氧化碳排放量。 在训练数据中&#xff0c;我们有 2019-2021 年的二氧化碳排放量 本笔记本的内容&am…

查看IIS管理器网站的日志和简要分析

一、查看IIS管理器网站的日志 1.1、查看网站的日志 1.2、IIS的4种日志文件格式 《1》 IIS日志文件格式 使用 Microsoft IIS 日志文件格式来记录有关站点的信息。这种格式由 HTTP.sys 进行处理,并且是固定的基于 ASCII 文本的格式,这意味着无法自定义记录的字段。字段由逗号…

【算法日志】动态规划刷题:完全背包应用问题(day39)

代码随想录刷题60Day 目录 前言 零钱兑换 完全平方数 前言 今天重点是对完全背包问题进一步了解&#xff0c;难度不大&#xff0c;重点是区分与其他背包问题在初始和遍历上的一些细节。 零钱兑换 int coinChange(vector<int>& coins, int amount) {if (!amount)re…

利用敏捷开发工具实现敏捷项目管理的实践经验分享

Scrum中非常强调公开、透明、直接有效的沟通&#xff0c;这也是“可视化的管理工具”在敏捷开发中如此重要的原因之一。通过“可视化的管理工具”让所有人直观的看到需求&#xff0c;故事&#xff0c;任务之间的流转状态&#xff0c;可以使团队成员更加快速适应敏捷开发流程。 …

Android scrollTo、scrollBy、以及scroller详解 自定义ViewPager

Scroller VelocityTracker VelocityTracker 是一个速度跟踪器&#xff0c;通过用户操作时&#xff08;通常在 View 的 onTouchEvent 方法中&#xff09;传进去一系列的 Event&#xff0c;该类就可以计算出用户手指滑动的速度&#xff0c;开发者可以方便地获取这些参数去做其他…

研磨设计模式day10中介者模式

目录 场景 思考 解决思路 模式讲解 调用示意图 中介者模式的优缺点 中介者模式的本质 何时选用 场景 如果没有主板&#xff0c;电脑各个配件怎么交互呢&#xff1f; 有些配件接口不同&#xff0c;必须把数据接口进行转换才能匹配上,无敌复杂。 有了主板之后就是下面这…

人工智能技术的主要类别

人工智能技术主要类别&#xff1a; 机器学习&#xff1a; 监督学习&#xff1a;使用带有标签的训练数据来训练模型&#xff0c;使其能够预测未知数据的标签。常见任务包括分类和回归。无监督学习&#xff1a;使用无标签的训练数据&#xff0c;模型通过发现数据中的模式、聚类或…

在idea上使用git的reset操作后,出现的四个选项Soft、Mixed、Hard、Keep选择说明

出现场景 选择已经commit的版本,点击Reset Current Branch to Here 然后便会出现下述四个选项 下面便对这个四个选项进行总结说明 原理 git revert是用于“反做”某一个版本,以达到撤销该版本的修改的目的 Soft Soft选项:在选择的回退点之后的所有更改将会保留并被gi…

预测下一波物联网网络安全挑战

本文讨论从孤立的物联网设置过渡到互连环境的复杂性&#xff0c;不断扩大的攻击面以及这种演变带来的微妙复杂性。 深入探讨标准化的紧迫性、级联故障的威胁以及利益相关者之间模糊的责任界限。 鉴于从孤立的物联网设备到互连的物联网环境的转变&#xff0c;这给网络安全带来…

研磨设计模式day12迭代器模式

目录 场景 解决方案 解决思路 代码示例 代码改造 Java实现迭代器 迭代器模式的优点 思考 何时选用 场景 大公司收购了一个小公司&#xff0c;大公司的工资系统采用List来记录工资列表&#xff0c;而小公司是采用数组&#xff0c;老板希望通过决策辅助系统来统一查看…

Axure RP

Axure RP 简介下载安装汉化注册 简介 Axure RP&#xff08;Rapid Prototyping&#xff09;是一款交互式原型设计工具&#xff0c;用于创建高保真的交互式界面原型和线框图。它主要用于用户体验&#xff08;UX&#xff09;和用户界面&#xff08;UI&#xff09;设计&#xff0c…

4.RabbitMQ高级特性 幂等 可靠消息 等等

一、如何保证生产者生产消息100%的投递成功 保障消息的成功发出保障MQ节点的成功接收发送端收到MQ节点&#xff08;Broker&#xff09;确认应答完善的消息进行补偿机制 1. 理解Confirm确认消息机制 消息的确认&#xff0c;是指生产者投递消息后&#xff0c;如果Broker收到消…

【C语言进阶(7)】内存函数 —— 使用方法 + 模拟实现

文章目录 前言Ⅰ 内存函数⒈memcpy⒉memmove⒊memcmp⒋memset Ⅱ 模拟实现⒈模拟实现 memcpy⒉模拟实现 memmove⒊模拟实现 memcmp⒋模拟实现 memset 前言 内存操作函数的优势 字符串函数只能操作字符串的内容&#xff0c;局限性很大。而内存函数可以操作任意类型的数据&…