Pytest测试框架一键动态切换测试环境实现思路及方案

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 一套测试脚本,能根据环境进行自动化的配置,省去手动配置参数的步骤,可以实现在多环境中运行,从而快速验证各个接口及相关服务在不同环境中的表现。

前言

在上一篇文章《Pytest fixture及conftest详解》中,我们介绍了fixture的一些关键特性、用法、作用域、参数等,本篇文章将结合fixture及conftest实现一键动态切换自动化测试环境。在开始前,我们可以先思考几个问题:动态切换测试环境的目的是什么(能够解决什么问题)?该如何实现(实现方案)?具体步骤是什么(实现过程)?

一、动态切换测试环境的目的是什么?

动态切换测试环境的目的是什么,或者说它能解决什么样的问题:

  • 便于快速验证功能在不同环境中的表现。比如:有的功能(背后的接口)在开发环境是正常的,但到了测试或预发布环境就出问题了,可以便于快速验证各个功能在不同环境中的表现;
  • 省去修改配置参数的繁琐步骤。通常情况下,我们的配置信息都是写在配置文件中,然后测试用例读取配置文件中不同的配置信息。如果想要切换环境,就需要修改配置文件或读取配置的逻辑。而动态切换测试环境则可以自动根据我们传入的命令行参数和预制好的读取配置的策略,自动识别、解析并返回对应的数据。
  • 为测试框架赋能。之前看过一篇文章《13条自动化测试框架设计原则》中说道:测试框架要能做到,一套脚本多环境运行,支持环境切换,并且能根据环境进行自动化的配置(包括系统配置、测试数据配置等)。

其实以上总结起来就是:一套测试脚本,能根据环境进行自动化的配置,省去手动配置参数的步骤,可以实现在多环境中运行,从而快速验证各个接口及相关服务在不同环境中的表现。

二、动态切换测试环境如何实现?

1.实现方案

我们希望:可以有个开关,自由控制执行脚本的运行环境,而不是需要我们手动修改,比如:选择dev时,自动读取的是开发环境的配置及测试数据:url、数据库配置、账号密码、测试数据;当切换到test时,自动读取的是开发环境的配置及测试数据。

大致实现原理如下所示:

  1. 用户通过pytest命令行传入参数驱动脚本执行(pytest_addoption用于实现自定义命令行参数);
  2. fixture函数get_env用于获取用户输入的命令行参数,传递给fixture.py中的各个fixture函数;
  3. fixture.py中的各个fixture函数根据get_env提供的环境参数值,解析测试环境对应的数据文件内容:URL(get_url)、账号(get_user)、数据库配置(get_db),同时传递给api类(api_module_A...B...C)、登录方法(login)、数据库连接方法(use_db)等,用于实例化操作,这部分fixture函数再传递给测试用例,用于用例前后置操作(相当于setup/teardown);
  4. 最后测试用例再根据各个fixture函数返回的实例对象、配置信息,调用各个模块的api函数,执行测试,并读写数据库实现数据校验、断言,从而最终实现切换环境策略;

2.目录结构&框架设计小技巧

1)目录结构

项目结构大致如下,至于目录结构和文件命名,只能说萝卜青菜各有所爱。比如有人喜欢把存放公共方法的common目录命名为utils,存放各个api模块的api目录命名为src......

2)自动化测试框架设计小技巧

  • api:存放封装各个项目、各个模块的api,如jk项目支付模块,可以命名为jk_pay.py;
  • config:存放配置文件,直接用py文件即可,不推荐使用ini、yaml,反而会多了一层解析,增大出错概率;
  • common:存放公共方法,如基于http协议requests库,则可以命名为http_requests.py;通过文件名称,大概率就能知道这个文件的作用,比如通过parse_excel的命名直接就能知道是解析excel文件;
  • main:框架主入口,存放用来批量执行用例的文件,比如:run_testcase_by_tag.py(前提是用例都打了标签)、run_testcase_by_name.py;
  • fixture:存放fixture文件,建议每个项目一个fixture文件,互不影响,如:jk_fixture.py、jc_fixture.py;
  • test_case:存放测试用例文件;
  • conftest.py:存放一些hook函数、全局fixture函数,如前面提到的自定义命令行参数的函数pytest_addoption、获取命令行参数的fixture函数get_env;
  • pytest.ini:pytest框架配置文件;

三、实现过程

上述的方案单从文字层面可能有些难以理解,下面我们结合具体的代码案例来详细讲述一下实现过程。

1.实现自定义命令行参数工具

在conftest.py中定义一个hook函数,实现自定义命令行工具,名为pytest_addoption(固定写法),用来在命令行中传入不同的环境参数;

defpytest_addoption(parser):
"""    添加命令行参数    parser.addoption为固定写法    default 设置一个默认值,此处设置默认值为test    choices 参数范围,传入其他值无效    help 帮助信息    """parser.addoption(
"--env", default="test", choices=["dev", "test", "pre"], help="enviroment parameter"    )

2.定义获取命令行参数的fixture函数

在conftest.py中定义get_env的fixture函数,用来获取用户在命令行输入的参数值,传递给fixture.py中的各个fixture函数。pytestconfig是request.config的快捷方式,所以request.config也可以写成pytestconfig。

@pytest.fixture(scope="session")
defget_env(request):
returnrequest.config.getoption("--env")

来测试一下命令行能否输入参数以及fixture函数get_env能否获取到。我们可以简单定义一个测试用例:

deftest_env(get_env):
print(f"The current environment is: {get_env}")

然后通过命令行执行此测试用例:

pytest-s-v--envdevtest_env.py::test_env

执行结果如下:

3.定义环境解析策略

例如当前项目为jc项目,则可以在fixture目录下定义一个jc_fixture.py的文件,用于专门存放此项目相关的fixture函数。fixture.py中的各个fixture函数根据get_env提供的环境参数值,解析测试环境对应的数据文件内容:URL(get_url)、账号(get_user)、数据库配置(get_db),同时传递给api类(api_module_A...B...C)进行实例化,登录方法(login)、数据库连接方法(use_db)等,进行初始化,这部分fixture函数再传递给测试用例,用于用例前后置操作(相当于setup/teardown);

importpytestfromconfig.configimportURLConf, PasswordConf, UsernameConf, ProductIDConffromapi.jc_commonimportJCCommonfromapi.jc_resourceimportJCResourcefromconfig.db_configimportDBConfigfromcommon.mysql_handlerimportMySQL@pytest.fixture(scope="session")
defget_url(get_env):
"""解析URL"""globalurlifget_env=="test":
print("当前环境为测试环境")
url=URLConf.RS_TEST_URL.valueelifget_env=="dev":
print("当前环境为开发环境")
url=URLConf.RS_DEV_URL.valueelifget_env=="pre":
print("当前环境为预发布环境")
url=URLConf.RS_PRE_URL.valuereturnurl@pytest.fixture(scope="session")
defget_user(get_env):
"""解析登录用户"""globalusername_admin, username_boss# 若get_env获取到的是test,则读取配置文件中测试环境的用户名ifget_env=="test":
username_admin=UsernameConf.RS_TEST_ADMIN.valueusername_boss=UsernameConf.RS_TEST_BOSS.value# 若get_env获取到的是dev,则读取配置文件中开发环境的用户名elifget_env=="dev":
username_admin=UsernameConf.RS_TEST_ADMIN.valueusername_boss=UsernameConf.RS_TEST_BOSS.value# 若get_env获取到的是pre,则读取配置文件中预发布环境的用户名elifget_env=="pre":
username_admin=UsernameConf.RS_TEST_ADMIN.valueusername_boss=UsernameConf.RS_TEST_BOSS.value@pytest.fixture(scope="session")
defget_db(get_env):
"""解析数据库配置"""globaldb_host, db_pwd, db_ssh_host, db_ssh_pwd, db_nameifget_env=="test":
db_host=DBConfig.db_test.get('host')
db_pwd=DBConfig.db_test.get('pwd')
db_ssh_host=DBConfig.db_test.get('ssh_host')
db_ssh_pwd=DBConfig.db_test.get('ssh_pwd')
db_name=DBConfig.db_test.get('dbname_jc')
elifget_env=="dev":
db_host=DBConfig.db_test.get('host')
db_pwd=DBConfig.db_test.get('pwd')
db_ssh_host=DBConfig.db_test.get('ssh_host')
db_ssh_pwd=DBConfig.db_test.get('ssh_pwd')
db_name=DBConfig.db_test.get('dbname_jc')
elifget_env=="pre":
db_host=DBConfig.db_test.get('host')
db_pwd=DBConfig.db_test.get('pwd')
db_ssh_host=DBConfig.db_test.get('ssh_host')
db_ssh_pwd=DBConfig.db_test.get('ssh_pwd')
db_name=DBConfig.db_test.get('dbname_jc')
@pytest.fixture(scope="session")
defjc_common(get_env, get_url):
"""传入解析到的URL、实例化jc项目公共接口类"""product_id=ProductIDConf.JC_PRODUCT_ID.valuejc_common=JCCommon(product_id=product_id, url=get_url)
returnjc_common@pytest.fixture(scope="session")
defjc_resource(get_env, get_url):
"""传入解析到的URL、实例化jc项目测试接口类"""product_id=ProductIDConf.JC_PRODUCT_ID.valuejc_resource=JCResource(product_id=product_id, url=get_url)
returnjc_resource@pytest.fixture(scope="class")
defrs_admin_login(get_user, jc_common):
"""登录的fixture函数"""password=PasswordConf.PASSWORD_MD5.valuelogin=jc_common.login(username=username_shipper, password=password)
shipper_user_id=login["b"]
returnadmin_user_id@pytest.fixture(scope="class")
defjc_get_admin_user_info(jc_common, jc_admin_login):
"""获取用户信息的fixture函数"""user_info=jc_common.get_user_info(user_id=rs_shipper_login)
shipper_cpy_id=user_info["d"]["b"]
returnadmin_cpy_id@pytest.fixture(scope="class")
defuse_db(get_db):
"""链接数据库的fixture函数"""mysql=MySQL(host=db_host, pwd=db_pwd, ssh_host=db_ssh_host, ssh_pwd=db_ssh_pwd, dbname=db_name)
yieldmysqlmysql.disconnect()

4.测试用例引用fixture

1)封装各个待测模块的api函数

登录模块:jc_common.py

fromcommon.http_requestsimportHttpRequestsclassJcCommon(HttpRequests):
def__init__(self, url, product_id):
super(JcCommon, self).__init__(url)
self.product_id=product_iddeflogin(self, username, password):
'''用户登录'''headers= {"product_id": str(self.product_id)}
params= {"a": int(username), "b": str(password)}
response=self.post(uri="/userlogin", headers=headers, params=params)
returnresponsedefget_user_info(self, uid, token):
'''获取用户信息'''headers= {"user_id": str(uid), "product_id": str(self.product_id), "token": token}
response=self.post(uri="/user/login/info", headers=headers)
returnresponse

业务模块:jc_resource.py

importrandomfromcommon.http_requestsimportHttpRequestsfromfakerimportFakerclassRSResource(HttpRequests):
def__init__(self, url, product_id):
super(RSResource, self).__init__(url)
self.product_id=product_idself.faker=Faker(locale="zh_CN")
defadd_goods(self, cpy_id, user_id, goods_name, goos_desc='', goods_type='', goos_price=''):
"""新增商品"""headers= {"product_id": str(self.product_id), "cpy_id": str(cpy_id), "user_id": str(user_id)}
params= {"a": goods_name, "b": goos_desc, "c": goods_type, "d": goos_price}
r=self.post(uri="/add/goods", params=params, headers=headers)
returnrdefmodify_goods(self, cpy_id, user_id, goods_name, goos_desc='', goods_type='', goos_price=''):
"""修改商品信息"""headers= {"product_id": str(self.product_id), "cpy_id": str(cpy_id), "user_id": str(user_id)}
params= {"a": car_name, "ab": car_id, "b": company_id, "c": car_or_gua}
r=self.post(uri="/risun/res/car/add/blacklist?md=065&cmd=006", params=params, headers=headers)
returnr

各个模块的api函数作为独立的存在,将配置与函数隔离,且不涉及任何fixture的引用。这样无论测试URL、用户名、数据库怎么变换,也无需修改待测模块的api函数,基本可以做到一劳永逸,除非接口地址和传参发生变化。

2)测试用例

JC项目的测试用例类TestJcSmoke根据各个jc_fixture.py中各个fixture函数返回的实例对象、配置信息,调用各个业务模块的api函数,执行测试,并读写数据库实现数据校验、断言;

importosimportsyssys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
importallurefromfixture.jc_fixtureimport*fromcommon.parse_excelimportParseExcellogger=LogGen("JC接口Smoke测试").getLog()
@allure.feature("JC项目接口冒烟测试")
classTestJcSmoke:
defsetup_class(self):
self.fake=Faker("zh_CN")
# 将fixture中的jc_resource实例、数据库实例、登录等fixture函数传递给测试用例进行调用@pytest.mark.jc_smoke@allure.story("商品管理")
deftest_01_goods_flow(self, jc_resource, jc_admin_login, jc_get_admin_user_info, use_db):
"""测试商品增删改查接口"""user_id=jc_admin_logincpy_id=jc_get_admin_user_infogoods_name="iphone 14pro max 512G"try:
logger.info(f"新增'{goods_name}'商品")
withallure.step("调用添加商品接口"):
add_goods=jc_resource.add_goods(cpy_id, user_id, goods_name, goods_type=1)
assertadd_goods["a"] ==200self.goods_id=add_goods["d"]
select_db=use_db.execute_sql(
f"SELECT * FROM goods_info WHERE company_id = {cpy_id} AND id = {self.goods_id}")  # 查询数据库是否存在新增的数据assertgoods_nameinstr(select_db)
logger.info(f"商品'{goods_name}'新增成功")
logger.info(f"修改'{goods_name}'的商品信息")
withallure.step("调用修改商品接口"):
modify_goods=jc_resource.modify_goods(cpy_id, user_id, goods_id=self.goods_id, goods_name=goods_name, goods_type=2)
assertmodify_goods["a"] ==200select_db=use_db.execute_sql(
f"SELECT goodsType FROM goods_info WHERE company_id = {cpy_id} AND id = {self.goods_id}")
assertstr(select_db[0]) =='2'logger.info(f"修改'{goods_name}'的商品信息成功")
logger.info(f"开始删除商品'{goods_name}'")
withallure.step("调用删除商品接口"):
del_goods=jc_resource.delete_goods(cpy_id, user_id, goods_id=self.goods_id)
assertdel_goods["a"] ==200select_db=use_db.execute_sql(
f"SELECT * FROM goods_info WHERE id = {self.goods_id}")
print(select_db)
logger.info(f"删除商品'{goods_name}'成功")
exceptAssertionErrorase:
logger.info(f"商品流程测试失败")
raisee

在上述smoke测试用例test_01_goods_flow中,同时验证了商品的增、改、删三个接口,形成一个简短的业务流,如果接口都是畅通的话,则最后会删除商品,无需再手动维护。

注:

1、上述模块接口及测试用例仅为演示使用,非真实存在。

2、传统的测试用例设计模式中,会把一些实例化放在setup或setup_class中,如:jc_resource = JcResource(xxx),但因为fixture函数无法在前后置方法中传递的缘故,所以要把一些实例化的操作放在fixture函数中进行,并return一个内存地址,直接传递给测试用例,从而使测试用例能够调用到实例对象中的业务api。

四、运行项目

完成了命令行参数、解析策略、封装接口、测试用例编写后,既可以直接在编辑器中点击运行按钮执行测试,也可以在命令行驱动执行。以下演示命令行执行用例方法:

  • -v:打印详细执行过程;
  • -s:控制台输出用例中的print语句;
  • --env:前面pytest_addoption定义的命令行参数,默认值:test,输入范围choices=["dev", "test", "pre"]

1.输入一个不存在的--env参数

pytest -v-s--env online test_jc_smoke.py

此时会提示我们参数错误,online为不可用选项。

2.运行测试环境

pytest -v-s--env test test_jc_smoke.py

为了方便起见,我直接运行了现有项目的测试用例,当传入test时,会在测试环境运行。

一共12条测试用例,全部运行通过:

同时,测试结果发送到企业微信群,关于自动化测试结果自动发送企业微信的实现思路,可参考前面分享过的一篇文章《利用pytest hook函数实现自动化测试结果推送企业微信

3.运行开发及预发布环境

pytest -v-s--env dev test_jc_smoke.py  # 开发环境pytest -v-s--env pre test_jc_smoke.py  # 预发布环境

dev、pre参数接收正常,不过因为开发、预发布环境没启动的缘故,所以执行失败。

五、Pytest实现一键切换环境方案原理小结

原理说明:

  • 测试环境变量由用户输入提供;
  • 测试框架定义测试数据解析函数,并根据用户输入的测试变量,解析并返回测试环境对应的数据文件内容;

当然,以上也并非最佳设计方案、实现起来也比较复杂,尤其是fixture模块的运用。如果你有更好的实现方案,欢迎讨论、交流!

相关文章
|
6天前
|
设计模式 前端开发 JavaScript
自动化测试框架设计原则与最佳实践####
本文深入探讨了构建高效、可维护的自动化测试框架的核心原则与策略,旨在为软件测试工程师提供一套系统性的方法指南。通过分析常见误区,结合行业案例,阐述了如何根据项目特性定制自动化策略,优化测试流程,提升测试覆盖率与执行效率。 ####
27 6
|
6天前
|
人工智能 前端开发 测试技术
探索软件测试中的自动化框架选择与优化策略####
本文深入剖析了当前主流的自动化测试框架,通过对比分析各自的优势、局限性及适用场景,为读者提供了一套系统性的选择与优化指南。文章首先概述了自动化测试的重要性及其在软件开发生命周期中的位置,接着逐一探讨了Selenium、Appium、Cypress等热门框架的特点,并通过实际案例展示了如何根据项目需求灵活选用与配置框架,以提升测试效率和质量。最后,文章还分享了若干最佳实践和未来趋势预测,旨在帮助测试工程师更好地应对复杂多变的测试环境。 ####
25 4
|
11天前
|
机器学习/深度学习 前端开发 测试技术
探索软件测试中的自动化测试框架选择与优化策略####
本文深入探讨了在当前软件开发生命周期中,自动化测试框架的选择对于提升测试效率、保障产品质量的重要性。通过分析市场上主流的自动化测试工具,如Selenium、Appium、Jest等,结合具体项目需求,提出了一套系统化的选型与优化策略。文章首先概述了自动化测试的基本原理及其在现代软件开发中的角色变迁,随后详细对比了各主流框架的功能特点、适用场景及优缺点,最后基于实际案例,阐述了如何根据项目特性量身定制自动化测试解决方案,并给出了持续集成/持续部署(CI/CD)环境下的最佳实践建议。 --- ####
|
12天前
|
Java 测试技术 持续交付
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
本文重点讲解如何搭建App自动化测试框架的思路,而非完整源码。主要内容包括实现目的、框架设计、环境依赖和框架的主要组成部分。适用于初学者,旨在帮助其快速掌握App自动化测试的基本技能。文中详细介绍了从需求分析到技术栈选择,再到具体模块的封装与实现,包括登录、截图、日志、测试报告和邮件服务等。同时提供了运行效果的展示,便于理解和实践。
48 4
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
|
11天前
|
测试技术 API Android开发
探索软件测试中的自动化框架选择与实践####
本文深入探讨了软件测试领域内,面对众多自动化测试框架时,如何依据项目特性和团队需求做出明智选择,并分享了实践中的有效策略与技巧。不同于传统摘要的概述方式,本文将直接以一段实践指南的形式,简述在选择自动化测试框架时应考虑的核心要素及推荐路径,旨在为读者提供即时可用的参考。 ####
|
15天前
|
测试技术 Android开发 UED
探索软件测试中的自动化框架选择
【10月更文挑战第29天】 在软件开发的复杂过程中,测试环节扮演着至关重要的角色。本文将深入探讨自动化测试框架的选择,分析不同框架的特点和适用场景,旨在为软件开发团队提供决策支持。通过对比主流自动化测试工具的优势与局限,我们将揭示如何根据项目需求和团队技能来选择最合适的自动化测试解决方案。此外,文章还将讨论自动化测试实施过程中的关键考虑因素,包括成本效益分析、维护难度和扩展性等,确保读者能够全面理解自动化测试框架选择的重要性。
32 1
|
18天前
|
Web App开发 定位技术 iOS开发
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
19 1
|
21天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
25天前
|
Web App开发 敏捷开发 存储
自动化测试框架的设计与实现
【10月更文挑战第20天】在软件开发的快节奏时代,自动化测试成为确保产品质量和提升开发效率的关键工具。本文将介绍如何设计并实现一个高效的自动化测试框架,涵盖从需求分析到框架搭建、脚本编写直至维护优化的全过程。通过实例演示,我们将探索如何利用该框架简化测试流程,提高测试覆盖率和准确性。无论你是测试新手还是资深开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
13天前
|
机器学习/深度学习 自然语言处理 物联网
探索自动化测试框架的演变与未来趋势
随着软件开发行业的蓬勃发展,软件测试作为保障软件质量的重要环节,其方法和工具也在不断进化。本文将深入探讨自动化测试框架从诞生至今的发展历程,分析当前主流框架的特点和应用场景,并预测未来的发展趋势,为软件开发团队选择合适的自动化测试解决方案提供参考。