Python pytest+allure的接口自动化测试框架

本文详细介绍了一个接口自动化测试项目的实现过程,包括项目结构、接口请求封装、测试案例编写、数据读取与验证,以及使用pytest和allure生成测试报告的具体操作。
一粒沙里见世界
一朵花里见天国
手掌里盛住无限
一刹那便是永劫

简单的项目结构

  • 运营平台项目
    在这里插入图片描述
  • baseApi:封装请求接口的api类,目前只使用到一个,代码如下
import requests


'''封装请求接口的函数,适合于需要校验登录权限的接口,
设置_headers为了减少再参数化用例的时候减少headers: xxx的步骤
加上**kwargs的参数为了请求接口便于传入其他参数,例如cookies= xx, files= xx
'''
def send(body, headers=None, **kwargs):
    if headers is None:
        headers = {
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Content-Type": "application/json"
        }
    return requests.request(**body, headers=headers, **kwargs)
  • report:存放接口报告的文件夹
  • testCase:存放测试案例
  • testData:存放测试数据,yaml的数据配置文件
  • testSuites:需要进行测试的类,或者说接口

文件详解在这里插入图片描述

  • conftest:测试案例中需要使用的前置请求比如登录,数据库连接请求等。
  • testCase的conftest文件代码
import pymssql


# 登录运营平台
@pytest.fixture(scope="class") #生效级别为class,详情请参照pytest,conftest文件的使用
def login():
    headers = {
        "User-Agent": "Chrome/10",
        "flag": "guns",
        "Content-Type": "application/json"
    }
    body = {
        "username": "rhcj-gbl",
        "password": "123456",
        "addressmac": "04:0E:3C:D2:D0:B7"
    }
    data = json.dumps(body)
    response = requests.post('http://登录的url/login', data=data, headers=headers)
    cookie = response.cookies
    return cookie

# 建立数据库连接
@pytest.fixture(scope="class") # 生效级别为class
def db():
    db = pymssql.connect("host", "dbhtnews_dev", "MqfUcGymupuTmsad", "DBHTNews_DEV")
    return db
  • testSuites及部分代码
    在这里插入图片描述
  • 注意,这里的request_body和cookie都是从testData里边进行读取的数据,这里只写两个参数即可,参数的值应该体现在testcase里边传过来
from baseAPI.base import send


# 获取资讯类型信息
def newsinfo_getType(request_body, cookie):
    response = send(request_body, cookies=cookie)
    return response

# 获取资讯子类型(等价类-科创板)
def newsinfo_getSubType(request_body, cookie):
    response = send(request_body, cookies=cookie)
    return response

# 资讯新建(等价类-二次编辑,科创板,无标题H5模板)
def newsinfo_add(request_body, cookie):
    response = send(request_body, cookies=cookie)
    return response

# 资讯查询(资讯标题查询)
def newsinfo_list_byTitle(request_body, cookie):
    response = send(request_body, cookies=cookie)
    return response
  • testCase及部分代码
    testCase
import pytest
import yaml
from testSuites import marketInfo
import allure
import json
from baseAPI.base import log


@allure.feature('市场信息菜单接口')  # 用于生成测试报告的模块说明
class TestMarketInfo:

    # 注意!此时yml文件中的格式必须是mapping格式,不然不能进行**解包
    with open('../testData/newsinfo.yml', 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)

    @allure.story('新闻资讯-获取资讯类型')  # 用于生成测试报告的功能说明
    @pytest.mark.parametrize('body', data['newsinfo_getType'])
    def test_newsinfo_getType(self, login, db, body):  # login参数为conftest文件的用户登录接口
        newsTypeLists = []  # 资讯类型列表
        newsOpenTypeLists = []  # 打开类型列表
        newsFuntypeLists = []  # 功能类型列表

        newsTypeDb = []  # 数据库查询资讯类型查询结果表
        openNewsTypeDb = []  # 数据库打开类型查询结果表
        funTypeDb = []  # 数据库功能类型查询结果表

        response = marketInfo.newsinfo_getType(body, login)  # login即:send函数中cookies=login,base中提到的**kwages作用显现
        resdict = json.loads(response.text)

        newsTypeList = resdict['newsTypeList']
        newsOpentypeList = resdict['newsOpentypeList']
        newsPushtitle = resdict['newsPushtitleList'][0]['typename']
        newsFuntypeList = resdict['newsFuntypeList']

        for i in newsTypeList:
            newsTypeLists.append(i['typename'])
        for j in newsOpentypeList:
            newsOpenTypeLists.append(j['typename'])
        for k in newsFuntypeList:
            newsFuntypeLists.append(k['typename'])

        try:
            # 创建数据库游标
            cursor = db.cursor()

            sql_newsType = 'select typename from t_news_type where deleted = 0'
            cursor.execute(sql_newsType)
            resultNewsType = cursor.fetchall()
            for i in resultNewsType:
                newsTypeDb.append(i[0])

            sql_openNewsType = 'select typename from t_news_opentype where deleted = 0'
            cursor.execute(sql_openNewsType)
            resultOpenNewsType = cursor.fetchall()
            for j in resultOpenNewsType:
                openNewsTypeDb.append(j[0])

            sql_newsFunType = 'select typename from t_news_funtype where deleted = 0'
            cursor.execute(sql_newsFunType)
            resultNewsFunType = cursor.fetchall()
            for k in resultNewsFunType:
                funTypeDb.append(k[0])

            cursor.close()

        except Exception:
            raise Exception('查询失败')
        pytest.assume(response.status_code == 200)
        pytest.assume(newsTypeLists == newsTypeDb)
        pytest.assume(newsOpenTypeLists == openNewsTypeDb)
        pytest.assume(newsPushtitle == '资讯')
        pytest.assume(newsFuntypeLists == funTypeDb)

    @allure.story('新闻资讯-获取资讯子类型')
    @pytest.mark.parametrize('body', data['newsinfo_getSubType'])
    def test_newsinfo_getSubType(self, login, db, body):
        subtypelists = []
        subTypeDb = []
        response = marketInfo.newsinfo_getSubType(body, login)
        print(response.json())
        resdict = json.loads(response.text)
        subtypelist = resdict["subtypeList"]

        for i in subtypelist:
            subtypelists.append(i['typename'])

        try:
            cursor = db.cursor()
            # 注意SqlServer的字符串要用单引号''包裹,不然会查询失败
            sql_subtype = "select typename from t_news_subtype where newstypeid='D003381E-FD30-43AD-BEC7-067E6D7360BC'' \
                                  'and deleted=0"
            cursor.execute(sql_subtype)
            resultSubType = cursor.fetchall()
            for i in resultSubType:
                subTypeDb.append(i[0])
            cursor.close()
        except Exception:
            raise Exception('查询失败')
        pytest.assume(response.status_code == 200)
        pytest.assume(subtypelists == subTypeDb)

    @allure.story('资讯新建接口')
    @pytest.mark.parametrize('body', data['newsinfo_add'])
    def test_newsinfo_add(self, login, body):
        response = marketInfo.newsinfo_add(body, login)
        pytest.assume(response.status_code == 200)

    @allure.story('资讯查询接口')
    @pytest.mark.parametrize('body', data['newsinfo_list'])
    def test_newsinfo_list(self, login, body):
        response = marketInfo.newsinfo_list_byTitle(body, login)
        pytest.assume(response.json()['rows'][0]['title'] == '接口测试-科创板')
        pytest.assume(response.status_code == 200)
  • @pytest.mark.parametrize是pytest传递参数的装饰器,这里表示从data(testData/newsinfo.yml)yml文件中读取,文件结构为{key1: [{key2: value2},{key3:value3}],[{},{}]}的形式,key1:当前接口所需要的测试数据key值(我这里想在一个yml文件中配置多条接口的入参文件,所以用字典的key来区分每个case应该调用的对应参数,如果不区分也可以,但是一个接口就需要一个yml配置文件,这样未免过于繁琐,不利于维护);key2;key3就表示该接口的body,也就是接口入参了,当一条case需要执行多种参数变化时(即在一个case里边执行多个不同入参的情况),可以在入参列表(或者元组)中,添加子列表入参,该装饰器会依次把每个子列表的入参传递给接口进行执行,相当于一个循环执行一个case的过程。详细的内容可以查看pytest的官方文档。pytest官方文档
  • pytest.assume()为断言方法,这里不用assert因为assert会再执行报错的时候就停止执行,而pytest.assume方法可以报错后保留报错信息,之后继续往下执行案例
  • testData部分入参信息,不用管data.py文件(这里我是用于调试读取文件用的)
    在这里插入图片描述
  • 举例案例中用到的是newsinfo.yml中的入参,所以—部分参数信息,如果是不需要入参的,比如get请求不需要参数进行查询,那么不写这部分的入参即可,即json为空
# 新建资讯,获取类型
newsinfo_getType:
-
  method: 'post'
  url: "接口url+路径"
  #这里只是查询接口,所以不需要json段的入参,这里可以不写
# 新建资讯, 获取资讯子类型
newsinfo_getSubType:
-
  method: 'post'
  url: '接口url+路径'
  json:
    newsTypeid: 'D003381E-FD30-43AD-BEC7-067E6D7360BC'

# 新建资讯(二次编辑,科创板,无标题 H5模板)
newsinfo_add:
# 二次编辑
-
  method: 'post'
  url: "接口url+路径"
  json:
    displatform: 1
    content: "<p>123</p>"
    ischangevido: False
    ismodifyvido: False
    deleted: False
    endtime: "2020-06-02 16:58:32"
    categoryArray:
      - "3F6F6B29-C1CE-400E-B048-175D0D28AF6B"
    istop: "0"
    dataTime:
      - "2020-06-01 16:58:32"
      - "2020-06-02 16:58:32"
    isdefault: False
    ispush: False
    newsfuntypeid: "00000000-0000-0000-0000-000000000000"
    newsopentypeid: "1796B11D-4AE5-4CF3-9731-DCF4C09AF657"
    newspushtitleid: "0DEF1DCA-C40B-45EC-93B9-9D43E50C4834"
    newssubtypeid: "B3D96CD3-9BA7-4F8F-98A2-385A6BFDF625"
    source: "xxx证券"
    starttime: "2020-06-01 16:58:32"
    title: "接口测试-二次编辑"
    isheadlines: "0"
    topstate: "0"
    showdisclaimer: "0"
    foothtml: "<div id=\"foothtml\" data-type=\"1\" data-url=\"\" style=\"background-color: #fff;color: #999;font-size: 16px;\"></div>"
# 科创板
-
  method: 'post'
  url: "接口url+路径"
  json:
    displatform: 2
    content: "<p>这是一条接口脚本数据<br/></p>"
    ischangevido: True
    ismodifyvido: False
    contentText: "这是一条接口脚本数据"
    deleted: False
    endtime: "2020-06-06 16:23:46"
    categoryArray:
      - "3F6F6B29-C1CE-400E-B048-175D0D28AF6B"
    istop: "0"
    dataTime:
      - "2020-06-05 16:23:46"
      - "2020-06-06 16:23:46"
    isdefault: False
    ispush: False
    newsfuntypeid: "00000000-0000-0000-0000-000000000000"
    newsopentypeid: "1796B11D-4AE5-4CF3-9731-DCF4C09AF657"
    newspushtitleid: "0DEF1DCA-C40B-45EC-93B9-9D43E50C4834"
    newssubtypeid: "377B3F93-E7DA-40FD-BAD2-4260826CE267"
    readtotal: "10"
    source: "xxx证券"
    starttime: "2020-06-05 16:23:46"
    title: "接口测试-科创板"
    videotitle: "科创板"
    isheadlines: "0"
    topstate: "1"
    topdataTime: ["2020-06-05 16:23:46", "2020-06-06 16:23:46"]
    startdate: "2020-06-05 16:23:46"
    enddate: "2020-06-06 16:23:46"
    showdisclaimer: "1"
    foothtml: "<div id=\"foothtml\" data-type=\"1\" data-url=\"\" style=\"background-color: #fff;color: #999;font-size: 16px;\"></div>"
#无标题H5模板
-
  method: 'post'
  url: "接口url+路径"
  json:
    displatform: 2
    content: "<p>我是一个无标题H5模板啊</p>"
    ischangevido: False
    ismodifyvido: False
    deleted: False
    endtime: "2020-06-30 10:13:57"
    categoryArray:
      - "3F6F6B29-C1CE-400E-B048-175D0D28AF6B"
    istop: "0"
    dataTime:
      - "2020-06-09 10:13:57"
      - "2020-06-30 10:13:57"
    isdefault: False
    ispush: False
    newsfuntypeid: "00000000-0000-0000-0000-000000000000"
    newsopentypeid: "1796B11D-4AE5-4CF3-9731-DCF4C09AF657"
    newspushtitleid: "0DEF1DCA-C40B-45EC-93B9-9D43E50C4834"
    newssubtypeid: "214DFFD7-9683-4048-99CD-31D636542BCB"
    readtotal: "10"
    source: "xxx证券"
    starttime: "2020-06-09 10:13:57"
    title: "接口测试-无标题H5模板"
    isheadlines: "0"
    topstate: "0"
    showdisclaimer: "0"
    foothtml: "<div id=\"foothtml\" data-type=\"1\" data-url=\"http://www.baidu.com\" style=\"background-color: #9FFC13;color: #363630;font-size: 16px;\">跳转百度</div>"

# 资讯查询
newsinfo_list:
-
  method: 'post'
  url: '接口url+路径'
  json:
    limit: 20
    offset: 0
    sort: ""
    order: ""
    newsOpenType: ""
    displatform: ""
    newsSubType: "377B3F93-E7DA-40FD-BAD2-4260826CE267"
    newsType: "D003381E-FD30-43AD-BEC7-067E6D7360BC"
    newsState: []
    state: ""
    title: "接口测试-科创板"
    modifydate: []
    modifyby: ""
    isheadlines: ""
    topstate: ""
  • 这里的数据也就是baseApi.base函数的body入参,返回值**body会对body整体进行解包,即实际传入接口的方式为:requests.request(method="post/get/update...", url="请求的接口", json="json格式的入参"/或者data="data格式的入参",**kwargs(这个根据自己需要的其他参数进行扩展加入))
  • body格式:
method: 请求方法(post, get, update...)
url: 请求的接口url(url+接口路径)
json: # json格式的入参,根据自己接口入参的格式,有可能是data类型,
  key1: value1
  key2: value2
  • 整个项目简单的架构就到这里,下边我们来看执行测试报告的部分,这里我们用allure的插件。

安装配置allure

  1. 安装allure(python)测试报告插件库:pip install allure-pytest
  2. 运行生成测试结果:pytest <测试目录> --alluredir <测试结果存放目录>例:pytest test --alluredir report/allure_origin注意!这个时候allure_origin里边的数据只是测试结果(json.txt文件),还不是测试报告
  3. 生成直观的测试报告:从官网下载allure,zip包,解压到相关目录(我这里是D:\Software\allure\lib)下,同时把加压后的bin目录添加到环境变量PATH中,就额可以用allure命令了,因为allure是java编写的,所以这里需要安装jkd环境,这里就不说怎么安装jdk了。
  4. 生成测试报告:allure generate <allure测试结果目录> -o <存放报录> --clean(清空旧数据)例:allure generate report/allure_origin -o report/allure_report --clean
  5. 在allure_report中可以看到有个index.html的文件,用浏览器打开,或者用命令allure open /report/allure_report(运行后会启动一个web服务用语展示报告)
  6. 效果示例:
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 特性场景即我们前边用@allure.teature()装饰的部分,里边各个接口的说明即@allure.story()装饰的部分

告辞!

<think>我们正在构建一个基于Python接口自动化测试框架,使用pytest和YAML。根据引用内容,框架的主要组件包括: - Python:主开发语言 - Requests:用于发送HTTP请求 - Pytest:测试执行与用管理 - YAML:用于管理测试数据 - Allure:生成测试报告(可选,但常见) - Logging:用于日志记录 搭建步骤一般包括: 1. 项目结构设计 2. 封装HTTP请求(使用requests) 3. 测试数据管理(YAML文件) 4. 测试用编写(使用pytest) 5. 测试执行与报告生成(pytest结合Allure) 6. 日志配置 下面我们按照步骤详细说明。 ### 1. 项目结构设计 典型的项目结构如下: ``` project/ ├── common/ # 公共模块 │ ├── __init__.py │ ├── request_util.py # 封装requests │ └── log_util.py # 日志配置 ├── testcases/ # 测试用 │ ├── __init__.py │ └── test_demo.py # 测试用文件 ├── testdata/ # 测试数据(YAML文件) │ └── demo_data.yml ├── conftest.py # pytest全局配置 ├── pytest.ini # pytest配置文件 └── requirements.txt # 依赖包 ``` ### 2. 封装HTTP请求 在`common/request_util.py`中,我们可以封装一个发送请求的类,如: ```python import requests import logging class RequestUtil: def __init__(self): self.session = requests.Session() def send_request(self, method, url, **kwargs): logging.info(f"请求方法: {method}, 请求URL: {url}, 请求参数: {kwargs}") resp = self.session.request(method, url, **kwargs) # 可以在这里添加统一的响应处理,比如状态码检查、日志记录等 logging.info(f"响应状态码: {resp.status_code}, 响应内容: {resp.text}") return resp ``` ### 3. 测试数据管理(YAML) YAML文件用于存储测试数据,如`testdata/demo_data.yml`: ```yaml test_case1: method: get url: http://httpbin.org/get params: key1: value1 key2: value2 expected_status: 200 ``` ### 4. 测试用编写 使用pytest编写测试用,结合YAML数据驱动。我们可以使用`pytest.mark.parametrize`来实现数据驱动。但是,为了更灵活地使用YAML,我们可以先读取YAML文件,然后参数化。 在`testcases/test_demo.py`中: ```python import pytest import yaml from common.request_util import RequestUtil # 读取YAML文件 def load_testdata(file_path): with open(file_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) return data # 获取测试数据 test_data = load_testdata('../testdata/demo_data.yml') class TestDemo: @pytest.mark.parametrize('case_name, case_data', test_data.items()) def test_api(self, case_name, case_data): req = RequestUtil() method = case_data['method'] url = case_data['url'] params = case_data.get('params', {}) expected_status = case_data['expected_status'] resp = req.send_request(method, url, params=params) assert resp.status_code == expected_status ``` ### 5. 配置pytest 在项目根目录创建`pytest.ini`文件: ```ini [pytest] addopts = -v --alluredir=./allure-results testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* ``` ### 6. 日志配置 在`common/log_util.py`中配置日志: ```python import logging import os def setup_logging(): log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' logging.basicConfig(level=logging.INFO, format=log_format) # 在项目初始化时调用 setup_logging() ``` ### 7. 安装依赖 创建`requirements.txt`文件: ``` requests pytest pyyaml allure-pytest ``` 安装依赖: ```bash pip install -r requirements.txt ``` ### 8. 运行测试并生成报告 运行测试: ```bash pytest ``` 生成Allure报告(需要先安装Allure命令行工具): ```bash allure serve ./allure-results ``` ### 9. 使用conftest.py进行全局配置 `conftest.py`可以用于定义pytest的fixture,如我们可以在这里初始化日志和请求工具。 ```python import pytest from common.log_util import setup_logging from common.request_util import RequestUtil @pytest.fixture(scope="session", autouse=True) def init_logging(): setup_logging() @pytest.fixture(scope="class") def req(): return RequestUtil() ``` 然后测试用中可以这使用: ```python class TestDemo: @pytest.mark.parametrize('case_name, case_data', test_data.items()) def test_api(self, req, case_name, case_data): method = case_data['method'] url = case_data['url'] params = case_data.get('params', {}) expected_status = case_data['expected_status'] resp = req.send_request(method, url, params=params) assert resp.status_code == expected_status ``` ### 注意事项 - 路径问题:读取YAML文件时,注意相对路径。上述示中,测试用文件在`testcases`目录,YAML文件在`testdata`目录,所以使用`../testdata/demo_data.yml`。也可以使用绝对路径,但建议通过配置文件管理路径。 - 封装扩展:根据实际需求,可以进一步封装断言、数据库操作等。 - 环境配置:可以设计多环境配置(如测试环境、生产环境),通过配置文件或命令行参数切换。 以上就是一个基本的框架搭建指南。根据实际项目需求,可以在此基础上进行扩展和优化。 §§相关问题§§ 1. 如何设计数据驱动测试,使得测试数据和测试用分离? 2. 在接口自动化测试中,如何处理登录状态的保持(如cookies或token)? 3. 如何使用Allure报告来增强测试结果的可视化? 4. 如何将接口自动化测试框架集成到持续集成(CI)系统中? 5. 在接口测试中,如何对接口返回的复杂数据结构(如嵌套JSON)进行断言?</think>### Python + Pytest + YAML 接口自动化测试框架搭建指南 #### 1. 框架核心组件 ```mermaid graph TD A[测试框架] --> B[Python] A --> C[Requests] A --> D[Pytest] A --> E[YAML] A --> F[Allure] A --> G[Logging] ``` #### 2. 搭建步骤 **步骤1:环境准备** ```bash # 创建虚拟环境 python -m venv venv source venv/bin/activate # 安装依赖 pip install requests pytest pyyaml allure-pytest pytest-html ``` **步骤2:项目结构设计** ``` project/ ├── common/ # 公共模块 │ ├── request_util.py # 请求封装 │ └── log_util.py # 日志工具 ├── testcases/ # 测试用 │ ├── __init__.py │ └── test_api.py # 测试脚本 ├── testdata/ # 测试数据 │ └── api_data.yml # YAML数据文件 ├── conftest.py # pytest全局配置 ├── pytest.ini # pytest配置文件 └── requirements.txt # 依赖清单 ``` **步骤3:封装HTTP请求(request_util.py)** ```python import requests import logging class RequestUtil: def __init__(self): self.session = requests.Session() logging.basicConfig(level=logging.INFO) def send_request(self, method, url, **kwargs): """统一请求入口""" logging.info(f"请求参数: {method} {url} {kwargs}") resp = self.session.request(method, url, **kwargs) logging.info(f"响应状态: {resp.status_code}, 响应内容: {resp.text[:200]}") return resp ``` **步骤4:YAML测试数据管理(testdata/api_data.yml)** ```yaml login_test: description: "用户登录接口测试" request: method: POST url: "/api/login" headers: Content-Type: "application/json" json: username: "testuser" password: "Pass123" validate: - eq: [status_code, 200] - contains: [content, "token"] ``` **步骤5:测试用编写(testcases/test_api.py)** ```python import pytest import yaml from common.request_util import RequestUtil # 加载YAML测试数据 def load_testdata(file_path): with open(file_path, 'r') as f: return yaml.safe_load(f) test_data = load_testdata('../testdata/api_data.yml') class TestAPI: @pytest.mark.parametrize("case_name", test_data.keys()) def test_api(self, case_name): data = test_data[case_name] req = RequestUtil() resp = req.send_request( data['request']['method'], data['request']['url'], headers=data['request'].get('headers', {}), json=data['request'].get('json', {}) ) # 断言验证 for validate in data['validate']: key, expected = validate['eq'] if key == "status_code": assert resp.status_code == expected elif key == "content": assert expected in resp.text ``` **步骤6:配置全局Fixture(conftest.py)** ```python import pytest @pytest.fixture(scope="session", autouse=True) def setup(): print("\n>>> 测试开始 <<<") yield print("\n>>> 测试结束 <<<") ``` **步骤7:Pytest配置(pytest.ini)** ```ini [pytest] addopts = -v --alluredir=./allure-results --html=report.html testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* ``` #### 3. 执行测试 ```bash # 运行测试 pytest # 生成Allure报告 allure serve allure-results # 生成HTML报告(直接查看report.html) ``` #### 4. 关键优化点 1. **参数化驱动**:通过YAML实现测试数据与代码分离 2. **动态关联**:使用`pytest`的`fixture`处理token传递 ```python @pytest.fixture def auth_token(): # 获取登录token并传递给测试用 return get_token() ``` 3. **异常处理**:在请求封装中添加重试机制 ```python from tenacity import retry, stop_after_attempt @retry(stop=stop_after_attempt(3)) def send_request(self, method, url, **kwargs): ... ``` 4. **多环境配置**:使用`pytest-base-url`插件切换测试环境 ```bash pytest --base-url https://api.test.com ``` #### 5. 框架优势 1. **可维护性**:YAML数据驱动便于非技术人员维护测试用 2. **扩展性**:模块化设计支持添加数据库校验、消息队列等组件 3. **可视化报告**:Allure提供多维度测试分析 4. **CI/CD集成**:通过`pytest`命令轻松接入Jenkins/GitHub Actions[^3]
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值