pytest自动化测试执行环境切换的两种解决方案

文章探讨了企业在项目中如何通过pytest的Hook方法和pytest_addoption来解决自动化测试中频繁环境切换的问题,提倡使用pytest-base-url插件进行更简便的一键式环境配置和测试执行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、痛点分析

  在实际企业的项目中,自动化测试的代码往往需要在不同的环境中进行切换,比如多套测试环境、预上线环境、UAT环境、线上环境等等,并且在DevOps理念中,往往自动化都会与Jenkins进行CI/CD,不论是定时执行策略还是迭代测试,那么问题来了,难道每次切换环境都需要提供一套测试代码?或者每次切换都要需修改我们的自动化环境配置?答案当然不是,不然怎么能叫测试自动化呢!

  在未使用pytest的Hook相关方法之前,切换环境我们一般会使用python的内置模块sys,它有一个argv的方法,可以从我们的命令行接收参数,通过它也可以实现上面的需求,就像如下部分代码这样:

try:
    if sys.argv[1] == '-p':
        # 预上线环境
        conf_url.write_value(filename=CONFIGS_FILE_PATH_URL,
                             data=p_api_address)
        modify_properties_url(filename=ENVIRONMENT_PROPERTIES_PATH,
                              url=p_api_address["request"]["url"])
    elif sys.argv[1] in [f't{i}' for i in range(1, 8)]:
        # 测试环境
        conf_url.write_value(filename=CONFIGS_FILE_PATH_URL,
                             data=test_api_address(sys.argv[1][1:]))
        modify_properties_url(filename=ENVIRONMENT_PROPERTIES_PATH,
                              url=test_api_address(sys.argv[1][1:])["request"]["url"])
    else:
        case_logger.error("Please confirm the environmental information.")
        print("Please confirm the environmental information.")
        raise Exception
except IndexError:
    # 生产环境
    conf_url.write_value(filename=CONFIGS_FILE_PATH_URL,
                         data=formal_api_address)
    modify_properties_url(filename=ENVIRONMENT_PROPERTIES_PATH,
                          url=formal_api_address["request"]["url"])

  这种方式我们需要使用 "python xxx.py -p" 这种方式进行环境切换,然后再执行pytest命令行去执行测试用例,但是这种方法能在pytest的命令行中使用吗?能不能将环境切换的命令行也集成到pytest的命令行中,执行整个自动化项目只使用一条命令行呢?

方法一:Hook方法pytest_addoption注册命令行参数

1、Hook方法注解

  • pytest_addoption:可以让用户注册一个自定义的命令行参数,方便用户将数据传递给 pytest
  • 这个 Hook 方法一般和内置 fixture pytestconfig 配合使用
  • pytest_addoption 注册命令行参数,pytestconfig 通过配置对象读取参数的值
  • 参考文档:Writing plugins — pytest documentation

2、使用方法

1.一般编写到conftest.py文件中

2.定义pytest_addoption方法注册pytest命令行参数,函数名和参数保持一致

def pytest_addoption(parser):
    """注册自定义参数 env 到配置对象"""
    parser.addoption("--env", action="store",
                     default="https://www.cnblogs.com/",
                     help="将命令行参数 ’--env' 添加到 pytest 配置中")

parser.addoption参数说明:

  • name:自定义命令行参数的名字,可以是:"xx", "-xx" 或 "--xx"
  • action:在命令行中遇到此参数时要采取的基本操作类型
    • - store:默认,只存储参数的值,可以存储任何类型的值,此时 default 也可以是任何类型的值,而且命令行参数多次使用也只能生效一个,最后一个值覆盖之前的值
    • - append:存储一个列表,用 append 模式将可以同时多次使用自定义参数,并且 default  默认值必须是一个列表,pytest 会把 default  默认参数的值和多个自定义参数的值放在一个列表中
    • - store_const:用 const 为命令行参数指定一个常量值,必须和 const 参数同时使用,使用这个模式后命令行参数不能赋值
  • - append_const:存储一个列表,使用 const 为命令行参数指定一个常量值,并将 default 默认值和 const  常量值添加到列表中,这个模式可以同时多次使用自定义参数,但是还是不能赋值,只能使用常量
  • nargs:应该使用的命令行参数的数量
  • const:某些操作和nargs选择所需的常量值
  • default:如果参数的值不在命令行中,则使用默认值
  • type:可以是 python 的基础类型,比如:int,str,float,list 等类型,如果不指定类型的话,pytest会把接受到的参数值都默认为 str 类型,所以我们有时需要指定参数的类型,在使用 type 指定类型时,也需要把 default 的类型修改为同样的类型!
  • choices:choices 可以指定几个值,自定义参数必须在这几个值中选择一个,否则会报错
  • required:命令行选项是否可以省略(仅可选)
  • help:对参数作用的简要说明
  • metavar:用法消息中参数的名称
  • dest:要添加到 parse_args() 返回的对象中的属性的名称

3.使用request.config.getoption("--命令行")获取命令行参数的值

@pytest.fixture(scope="session")
def get_env(request):
    """从配置对象中读取自定义参数的值"""
    return request.config.getoption("--env")

4.fixture继承,进行环境切换

@pytest.fixture(autouse=True)
def set_env(get_env):
    """将自定义参数的值写入全局配置文件"""
    with open(ENV_TXT_FILE, 'w', encoding='utf-8') as f:
        f.write(get_env)

5.通过pytest命令行传参一键化实现环境切换和测试用例执行

方法二:使用插件pytest-base-url进行命令行传参

1.安装pytest-base-url

pip install pytest-base-url -i https://pypi.douban.com/simple

2.将base_url参数传入到fixture函数中

@pytest.fixture
def driver_setup(base_url):
    try:
        URL = base_url
        start_chrome(URL, options=browser_options(), headless=False)
        driver = get_driver()
    except Exception as e:
        log.error(e)
    else:
        yield driver

3.pytest命令行传参

  使用 --base-url https://www.xxx.com/ 形式传参

pytest --base-url https://www.cnblogs.com/

第一种不局限于环境切换,任何想通过pytest命令行传入自定义的参数都可以通过方法一实现,因此杀鸡焉用牛刀,环境切换优先推荐方法二!

【整整200集】超超超详细的python自动化测试进阶教程合集,真实模拟企业项目实战

<think>我们被要求处理在pytest中读取pytest.ini和yaml文件的问题。首先,我们需要知道pytest.ini是pytest的配置文件,通常用于存储项目配置。而yaml文件常用于存储测试数据。下面将分别介绍如何读取这两种文件。 ### 读取pytest.ini pytest.ini文件位于项目根目录,pytest运行时会自动读取其中的配置。我们可以使用pytest的内置对象`pytest`来访问配置项。 示例pytest.ini文件: ```ini [pytest] log_level = INFO base_url = https://api.example.com ``` 在测试代码中读取: ```python import pytest def test_read_ini(pytestconfig): # pytestconfig 是内置的fixture,用于访问配置 base_url = pytestconfig.getini('base_url') print(f"Base URL: {base_url}") assert base_url != "" ``` ### 读取yaml文件 yaml文件通常用于存储测试数据。我们可以使用PyYAML库来读取。首先需要安装:`pip install pyyaml`。 示例数据文件(data.yaml): ```yaml test_data: - username: "user1" password: "pass1" expected: 200 - username: "user2" password: "pass2" expected: 401 ``` 在测试中读取: ```python import yaml import pytest def load_test_data(): with open('data.yaml', 'r') as f: return yaml.safe_load(f)['test_data'] @pytest.mark.parametrize("data", load_test_data()) def test_login(data): # 使用data中的数据进行测试 print(f"Testing with username: {data['username']}") # 模拟请求和断言 assert data['expected'] == 200 # 实际中根据请求结果断言 ``` ### 结合pytest.ini指定yaml文件路径 我们还可以将yaml文件的路径放在pytest.ini中,然后在测试中读取这个路径,再加载yaml文件。 pytest.ini: ```ini [pytest] test_data_file = test_data/data.yaml ``` 测试代码: ```python import yaml import pytest @pytest.fixture(scope="session") def test_data(pytestconfig): file_path = pytestconfig.getini('test_data_file') with open(file_path, 'r') as f: return yaml.safe_load(f)['test_data'] @pytest.mark.parametrize("data", test_data) def test_example(data): # 使用data ``` ### 注意事项 1. 读取yaml文件时,使用`safe_load`避免安全问题。 2. 路径问题:如果yaml文件路径是相对路径,注意当前工作目录。 ### 完整示例结构 ``` project/ ├── pytest.ini ├── test_data/ │ └── data.yaml └── tests/ └── test_example.py ``` §§相关问题§§ 1. 在pytest中,如何动态地从pytest.ini读取配置并应用于参数化测试? 2. 使用yaml文件存储测试数据时,如何处理文件路径的跨平台兼容性问题? 3. 除了yaml,pytest还支持哪些格式的测试数据文件?如何实现? 4. 如何利用pytest钩子函数在测试启动前预加载yaml数据? 5. 在并发执行测试时,读取yaml文件需要注意哪些线程安全问题?</think>### 在pytest中处理配置文件的最佳实践 以下是读取pytest.ini和YAML文件的完整解决方案,包含代码示例和架构设计: #### 一、读取pytest.ini配置文件 ```python # conftest.py import pytest from configparser import ConfigParser def pytest_configure(config): """pytest启动时自动加载配置文件""" global CONFIG CONFIG = ConfigParser() CONFIG.read('pytest.ini') # 读取配置文件 @pytest.fixture(scope="session") def env_config(): """获取环境配置""" return { 'base_url': CONFIG.get('environments', 'base_url'), 'timeout': CONFIG.getint('settings', 'request_timeout', fallback=10) } # 测试用例中使用 def test_api_endpoint(env_config): base_url = env_config['base_url'] response = requests.get(f"{base_url}/api/v1/users", timeout=env_config['timeout"]) assert response.status_code == 200 ``` #### 二、读取YAML测试数据文件 ```python # utils/data_loader.py import yaml import os def load_yaml(file_path): """加载YAML文件""" with open(file_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) # 测试数据文件示例 (test_data/login_cases.yaml) """ - case_name: "管理员登录成功" username: "admin@example.com" password: "Admin@123" expected_code: 200 - case_name: "密码错误" username: "user@test.com" password: "wrong_pass" expected_code: 401 """ # conftest.py @pytest.fixture def test_data(request): """动态加载YAML测试数据""" data_file = os.path.join("test_data", f"{request.function.__name__}.yaml") return load_yaml(data_file) # 测试用例中使用参数化 @pytest.mark.parametrize("data", test_data) def test_login(data): response = login_api(data['username'], data['password']) assert response.status_code == data['expected_code'] ``` #### 三、组合配置文件与测试数据 ```python # config_manager.py from configparser import ConfigParser import yaml class ConfigManager: def __init__(self): self.config = ConfigParser() self.config.read('pytest.ini') def get_env(self, env_name='staging'): """获取环境配置""" return { 'api_url': self.config.get(env_name, 'api_url'), 'db_host': self.config.get(env_name, 'db_host') } @staticmethod def load_test_data(file_name): """加载测试数据文件""" with open(f"test_data/{file_name}.yaml") as f: return yaml.safe_load(f) # conftest.py @pytest.fixture(scope="session") def config(): return ConfigManager() # 测试用例中使用 def test_order_creation(config): env = config.get_env() test_cases = config.load_test_data("order_cases") for case in test_cases: response = requests.post( f"{env['api_url']}/orders", json=case['payload'], timeout=10 ) assert response.json()['status'] == case['expected_status'] ``` #### 四、高级功能实现 1. **环境变量覆盖配置** ```python # conftest.py import os @pytest.fixture(scope="session") def dynamic_config(): config = ConfigParser() config.read('pytest.ini') # 环境变量优先覆盖 if 'API_URL' in os.environ: config.set('production', 'api_url', os.environ['API_URL']) return config ``` 2. **多环境配置切换** ```ini # pytest.ini [environments] default = staging [staging] api_url = https://stage-api.example.com db_host = db-stage.internal [production] api_url = https://api.example.com db_host = db-prod.internal ``` ```python # conftest.py def pytest_addoption(parser): parser.addoption("--env", action="store", default="staging") @pytest.fixture(scope="session") def env_config(request): env_name = request.config.getoption("--env") config = ConfigParser() config.read('pytest.ini') return dict(config[env_name]) ``` #### 五、企业级最佳实践 1. **配置文件校验** ```python # utils/config_validator.py from schema import Schema, And CONFIG_SCHEMA = Schema({ 'api_url': And(str, len), 'request_timeout': And(int, lambda n: 0 < n <= 30), 'retry_count': And(int, lambda n: n >= 0) }) def validate_config(config): return CONFIG_SCHEMA.validate(config) ``` 2. **配置热重载** ```python # config_monitor.py import time from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class ConfigChangeHandler(FileSystemEventHandler): def on_modified(self, event): if event.src_path.endswith('pytest.ini'): reload_config() def start_config_monitor(): observer = Observer() observer.schedule(ConfigChangeHandler(), path='.', recursive=False) observer.start() ``` 3. **安全配置管理** ```python # secure_config.py from cryptography.fernet import Fernet def encrypt_config(): cipher = Fernet(os.environ['CONFIG_KEY']) with open('pytest.ini', 'rb') as f: encrypted = cipher.encrypt(f.read()) with open('config.enc', 'wb') as f: f.write(encrypted) def decrypt_config(): cipher = Fernet(os.environ['CONFIG_KEY']) with open('config.enc', 'rb') as f: return cipher.decrypt(f.read()).decode() ``` > **关键建议**: > 1. 使用`configparser`的`fallback`参数处理缺失配置项 > 2. 对YAML文件使用`safe_load`防止安全漏洞 > 3. 通过`pytest-addoption`实现命令行参数覆盖 > 4. 重要配置添加校验机制防止运行时错误
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值