pip安装时能够经过-i https://pypi.doubanio.com/simple/
,指定使用豆瓣的源, 下载稍微快一点html
pip install requests pymysql pyyaml pytest pyetst-xdist pytest-check pytest-rerunfailures pytest-base-url pytest-html pytest-level pytest-timeout -i https://pypi.doubanio.com/simple/
导出依赖到requirements.txt中python
pip freeze > requirments.txt
分层设计模式: 每一层为上层提供服务mysql
用例层(测试用例)
| Fixtures辅助层(全局的数据、数据库操做对象和业务流等) | utils实用方法层(数据库操做, 数据文件操做,发送邮件方法等等)
longteng17/ - data/ - data.yaml: 数据文件 - reports/: 报告目录 - test_cases/: 用例目录 - pytest.ini: pytest配置 - api_test/: 接口用例目录 - conftest.py: 集中管理Fixtures方法 - web_test/: web用例目录 - app_test/: app用例目录 - utils/: 辅助方法 - data.py: 封装读取数据文件方法 - db.py: 封装数据库操做方法 - api.py: 封装api请求方法 - notify.py: 封装发送邮件等通知方法 - conftest.py: 用来放置通用的Fixtures和Hooks方法 - pytest.ini: Pytest运行配置
规划conftest.py的位置,要确保项目跟目录被导入到环境变量路径(sys.path)中去。
conftest.py及用例的导入机制为:web
- 若是在包(同级有init.py)内,则导入最上层包(最外一个包含init.py)的父目录。
- 若是所在目录没有init.py,直接导入conftest.py父目录。
因为用例数据经常须要多层级的数据结构,这里选择yaml文件做为本项目的数据文件,示例格式以下:sql
test_case1: a: 1 b: 2
数据第一层以用例名标识某条用例所使用的数据,这里约定要和用例名称彻底一致,方便后面使用Fixture方法自动向用例分配数据。数据库
标记: mark, 也称做标签, 用来跨目录分类用例方便灵活的选择执行。json
也能够根据本身的需求,按模块、按是否有破坏性等来标记用例。flask
首先须要安装pyyaml, 安装方法:pip install pyyaml
读取yaml文件数据的方法为:设计模式
yaml.safe_load()和yaml.load()的区别:api
因为yaml文件也支持任意的Python对象
从文件中直接加载注入Python是极不安全的, safe_load()会屏蔽Python对象类型,只解析加载字典/列表/字符串/数字等级别类型数据
示例以下:
import yaml def load_yaml_data(file_path): with open(file_path, encoding='utf-8') as f: data = yaml.safe_load(f) print("加载yaml文件: {file_path} 数据为: {data}") return data
为了示例简单,这里没有对文件不存在、文件格式非yaml等异常作处理。异常处理统一放到Fixture层进行。
假如项目要支持多种数据文件, 可使用类来处理。
这里使用pymysql, 安装方法pip install pymysql
数据库密码等敏感数据,直接放在代码或配置文件中,会有暴露风险,用户敏感数据咱们能够放到环境变量中,而后从环境变量中读取出来。
注意:部署项目时,应记得在服务器上配置相应的环境变量,才能运行。
Windows在环境变量中添加变量MYSQL_PWD,值为相应用户的数据库密码,也能够将数据库地址,用户等信息也配置到环境变量中。
Linux/Mac用户能够经过在/.bashrc或/.bash_profile或/etc/profile中添加
export MYSQL_PWD=数据库密码
而后source相应的文件使之生效,如source ~/.bashrc
。
Python中使用os.getenv('MYSQL_PWD')
即可以拿到相应环境变量的值。
注意:若是使用PyCharm,设置完环境变量后,要重启PyCharm才能读取到新的环境变量值。
咱们使用字典来存储整个数据库的配置,而后经过字典拆包传递给数据库链接方法。
import os import pymysql DB_CONF = { 'host': '数据库地址', 'port': 3306, 'user': 'test', 'password': os.getenv('MYSQL_PWD'), 'db': 'longtengserver', 'charset': 'utf8' } conn = pymysql.connect(**DB_CONF)
数据常见的操做方法有查询,执行修改语句和关闭链接等。对应一种对象的多个方法,咱们使用类来封装。
同时为避免查询语句和执行语句的串扰,咱们在创建链接时使用autocommit=True来确保每条语句执行后都当即提交,完整代码以下。
import os import pymysql DB_CONF = { 'host': '数据库地址', 'port': 3306, 'user': 'test', 'password': os.getenv('MYSQL_PWD'), 'db': 'longtengserver', 'charset': 'utf8' } class DB(object): def __init__(self, db_conf=DB_CONF) self.conn = pymysql.connect(**db_conf, autocommit=True) self.cur = self.conn.cursor(pymysql.cursors.DictCursor) def query(self, sql): self.cur.execute(sql) data = self.cur.fetchall() print(f'查询sql: {sql} 查询结果: {data}') return data def change_db(self, sql): result = self.cur.execute(sql) print(f'执行sql: {sql} 影响行数: {result}') def close(self): print('关闭数据库链接') self.cur.close() self.conn.close()
其中若是查询中包含中文,要根据数据库指定响应的charset,这里的charset值为utf8不能写成utf-8。
self.conn.cursor(pymysql.cursors.DictCursor)这里使用了字典格式的游标,返回的查询结果会包含响应的表字段名,结果更清晰。
因为全部sql语句都是单条自动提交,不支持事务,所以在change_db时,不须要再做事务异常回滚的操做,对于数据库操做异常,统一在Fixture层简单处理。
# db.py ... class FuelCardDB(DB): def del_card(self, card_number): print(f'删除加油卡: {card_number}') sql = f'DELETE FROM cardinfo WHERE cardNumber="{card_number}"' self.change_db(sql) def check_card(self, card_number): print(f'查询加油卡: {card_number}') sql = f'SELECT id FROM cardinfo WHERE cardNumber="{card_number}"' res = self.query(sql) return True if res else False def add_card(self, card_number): print(f'添加加油卡: {card_number}') sql = f'INSERT INTO cardinfo (cardNumber) VALUES ({card_number})' self.change_db(sql)
发送邮件通常要经过SMTP协议发送。首先要在你的邮箱设置中开启SMTP服务,清楚SMTP服务器地址、端口号已是否必须使用安全加密传输SSL等。
使用Python发送邮件分3步:
from email.mime.text import MIMEText import smtplib body = 'Hi, all\n附件中是测试报告, 若有问题请指出' body2 = '<h2>测试报告</h2><p>如下为测试报告内容<p>' # msg = MIMEText(content, 'plain', 'utf-8') msg = MIMEText(content2, 'html', 'utf-8')
使用MIMEText组装Email消息数据对象,正文支持纯文本plain和html两种格式。
...
msg['From'] = 'zhichao.han@qq.com' msg['To'] = 'superhin@126.com' msg['Subject'] = '接口测试报告'
msg['From']中也能够声明收件人名称,格式为:
msg['From'] = '<韩志超> zhichao.han@qq.com'
msg['To']中也能够写多个收件人,写到一个字符串中使用英文逗号隔开:
msg['To'] = 'superhin@126.com,ivan-me@163.com'
注意邮件头的From、To只是一种声明,并不必定是实际的发件人和收件人,好比From写A邮箱,实际发送时,使用B邮箱的SMTP发送,便会造成代发邮件(B表明A发送)。
...
smtp = smtplib.SMTP('邮箱SMTP地址') # smtp = smtplib.SMTP_SSL('邮箱SMTP地址') smtp.login('发件人邮箱', '密码') smtp.sendmail('发件人邮箱', '收件人邮箱', msg.as_string())
这里登陆SMTP和SMTP_SSL要看邮箱服务商支持哪一种,链接时也能够指定端口号,如:
smtp = smtplib.SMTP_SSL('邮箱SMTP地址', 465)
登陆时的密码根据邮箱的支持能够是受权码或登陆密码(通常如QQ邮箱采用受权码,不支持使用登陆密码登陆SMTP)。
sendmail发送邮件时,使用的发件人邮箱和收件人邮箱是实践的发件人和收件人,能够和邮件头中的不一致。可是发件人邮箱必须和登陆SMTP的邮箱一致。
sendmail每次只能给一个收件人发送邮件,当有多个收件人是,可使用屡次sendmail方法,示例以下:
receivers = ['superhin@163.com', 'zhichao.han@qq.com'] for person in receivers: smtp.sendmail('发件人邮箱', person, msg.as_string())
msg.as_string()是将msg消息对象序列化为字符串后发送。
因为邮件正文会过滤掉大部分的样式和JavaScript,所以直接将html报告读取出来,放到邮件正文中每每没有任何格式。这时,咱们能够经过附件来发送测试报告。
邮件附件通常采用二进制流格式(application/octet-stream),正文则采用文本格式。要混合两种格式咱们须要使用MIMEMultipart这种混合的MIME格式,通常步骤为:
示例代码以下:
from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import smtplib # 1. 创建一个MIMEMultipart消息对象 msg = MIMEMultipart() # 2. 添加邮件正文 body = MIMEText('hi, all\n附件中是测试报告,请查收', 'plain', 'utf-8') msg.attach(body) # 3. 添加附件 att = MIMEText(open('report.html', 'rb').read(), 'base64', 'utf-8') att['Content-Type'] = 'application/octet-stream' att["Content-Disposition"] = 'attachment; filename=report.html' msg.attach(att1) # 4. 添加邮件头信息 ... # 5. 发送邮件 ...
使用消息对象msg的attach方法来添加MIMEText格式的邮件正文和附件。
构造附件MIMEText对象时,要使用rb模式打开文件,使用base64格式编码,同时要声明附件的内容类型Content-Type以及显示排列Content-Dispositon,这里的attachment; filename=report.html
,attachment表明附件图标,filename表明显示的文件名,这里表示图标在左,文件名在右,显示为report.html。
添加邮件头信息和发送邮件同发送普通邮件一致。
一样,咱们能够将敏感信息邮箱密码配置到环境变量中去,这里变量名设置为SMTP_PWD。
import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import smtplib SMTP_HOST = '邮箱SMTP地址' SMTP_USER = '发件人邮箱' SMTP_PWD = os.getenv('SMTP_PWD') def send_email(self, body, subject, receivers, file_path): msg = MIMEMultipart() msg.attach(MIMEText(body, 'html', 'utf-8')) att1 = MIMEText(open(file_path, 'rb').read(), 'base64', 'utf-8') att1['Content-Type'] = 'application/octet-stream' att1["Content-Disposition"] = f'attachment; filename={file_name}' msg.attach(att1) msg['From'] = SMTP_USER msg['To'] = ','.join(receivers) msg['Subject'] = subject smtp = smtplib.SMTP_SSL(SMTP_HOST) smtp.login(SMTP_USER, SMTP_PWD) for person in receivers: print(f'发送邮件给: {person}') smtp.sendmail(SMTP_USER, person, msg.as_string()) print('邮件发送成功')
一样,为了示例简单,这里并无对SMTP链接、登陆、发送邮件作异常处理,读者能够进行相应的补充。
requests自己已经提供了很好的方法,特别是通用的请求方法requests.request()。这里的封装只是简单加了base_url组装、默认的超时时间和打印信息。
import requests TIMEOUT = 30 class Api(object): def __init__(self, base_url=None): self.session = requests.session() self.base_url = base_url def request(self, method, url, **kwargs): url = self.base_url + url if self.base_url else url kwargs['timeout'] = kwargs.get('timeout', TIMEOUT) res = self.session.request(method, url, **kwargs) print(f"发送请求: {method} {url} {kwargs} 响应数据: {res.text}") return res def get(self, url, **kwargs): return self.request('get', url, **kwargs) def post(self, url, **kwargs): return self.request('post', url, **kwargs)
这里,Api实例化时若是传递了base_url参数,则全部的url都会拼接上base_url。kwargs['timeout'] = kwargs.get('timeout', TIMEOUT)
,设置默认的超时时间设置为30s。
import pytest from utils.data import Data from utils.db import FuelCardDB from utils.api import Api @pytest.fixture(scope='session') def data(request): basedir = request.config.rootdir try: data_file_path = os.path.join(basedir, 'data', 'api_data.yaml') data = Data().load_yaml(data_file_path) except Exception as ex: pytest.skip(str(ex)) else: return data @pytest.fixture(scope='session') def db(): try: db = FuelCardDB() except Exception as ex: pytest.skip(str(ex)) else: yield db db.close() @pytest.fixture(scope='session') def api(base_url): api = Api(base_url) return api
这里对,utils实用方法层的异常进行简单的skip处理,即当数据链接或数据文件有问题时,全部引用该Fixture的用例都会自动跳过。
在api这个Fixtures中咱们引入了base_url,它来自于插件pytest-base-url,能够在运行时经过命令行选项--base-url或pytest.ini中的配置项base_url来指定。
[pytest] ... base_url=http://....:8080
Fixture方法经过用例参数,注入到用例中使用。Fixture方法中能够拿到用例所在的模块,模块变量,用例方法对象等数据,这些数据都封装在Fixture方法的上下文参数request中。
原有的data这个Fixture方法为用例返回了数据文件中的全部数据,可是通常用例只须要当前用例的数据便可。咱们在数据文件中第一层使用和用例方法名同名的项来区分各个用例的数据。如:
# api_data.yaml test_add_fuel_card_normal: data_source_id: bHRz cardNumber: hzc_00001 ...
下面的示例演示了根据用例方法名分配数据的Fixture方法:
# conftest.py ... @pytest.fixture def case_data(request, data): case_name = request.function.__name__ return data.get(case_name)
request是用例请求Fixture方法的上下文参数,里面包含了config对象、各类Pytest运行的上下文信息,能够经过Python的自省方法print(request.__dict__)
查看request对象中全部的属性。
这样,用例中引入的case_data参数就只是该用例的数据。
一条完整的用例应包含如下步骤:
另一般用例还应加上指定的标记。
import pytest @pytest.mark.level(1) @pytest.mark.api def test_add_fuel_card_normal(api, db, case_data): """正常添加加油卡""" url = '/gasStation/process' data_source_id, card_number = case_data.get('data_source_id'), case_data.get('card_number') # 环境检查 if db.check_card(card_number): pytest.skip(f'卡号: {card_number} 已存在') json_data = {"dataSourceId": data_source_id, "methodId": "00A", "CardInfo": {"cardNumber": card_number}} res_dict = api.post(url, json=json_data).json() # 响应断言 assert 200 == res_dict.get("code")) assert "添加卡成功" == res_dict.get("msg") assert res_dict.get('success') is True # 数据库断言 assert db.check_card(card_number) is True # 环境清理 db.del_card(card_number)
使用assert断言时,当某一条断言失败后,该条用例即视为失败,后面的断言不会再进行判断。有时咱们须要每一次能够检查全部的检查点,输出全部断言失败项。此时咱们可使用pytest-check插件进行复合断言。
安装方法pip install pytest-check。
所谓复合断言即,当某条断言失败后仍继续检查下面的断言,最后汇总全部失败项。
pytest-check使用方法
import pytest_check as check ... check.equal(200, es_dict.get("code")) check.equal("添加卡成功",res_dict.get("msg")) check.is_true(res_dict.get('success')) check.is_true(db.check_card(card_number))
除此外经常使用的还有:
若是某些用例暂时环境不知足没法运行能够标记为skip, 也可使用skipif()判断条件跳过。 对于已知Bug,还没有完成的功能也能够标记为xfail(预期失败)。
使用方法以下:
import os import pytest @pytest.mark.skip(reason="暂时没法运行该用例") def test_a(): pass @pytest.mark.skipif(os.getenv("MYSQL_PWD") is None, reason="缺失环境变量MYSQL_PWD配置") def test_b(): pass @pytest.mark.xfail(reason='还没有解决的已知Bug') def test_c(): pass
test_b首先对环境变量作了检查,若是没有配置MYSQL_PWD这个环境变量,则会跳过该用例。
test_c为指望失败,这时若是用例正常经过则视为异常的xpass状态,失败则为视为正常的xfail状态,在--strict严格模式下,xfail视为用例经过,xpass视为用例失败。
这里标记运行时分别使用-r/-x/-X显示skip、xfail、xpass的缘由说明:
pytest -rsxX
这里的-s能够在命令行上显示用例中print的一些信息。
另外,也能够在Fixture方法或用例中,使用pytest.skip("跳过缘由"), pytest.xfail("指望失败缘由")来根据条件表用例跳过和指望失败。
标记skip和xfail属于一种临时隔离策略,等问题修复后,应及时去掉该标记。
运行时经过传入--base-url
来切换环境:
pytest --base-url=http://服务地址:端口号
默认Pytest支持-lf参数来重跑上次失败的用例。但若是咱们想要本次用例失败后自动重跑的话,可使用pytest-rerunfailures插件。
安装方法pip install pytest-rerunfailures。
运行时使用
pytest --reruns 3 --reruns-delay 1
来指定失败用例延迟1s后自动重跑,最多重跑3次。
对应已知的不稳定用例,咱们能够经过flasky标记,来使之失败时自动重跑,示例以下。
import pytest @pytest.mark.flaky(reruns=3, reruns_delay=1) def test_example(): import random assert random.choice([True, False])
使用pytest-level能够对用例标记等级,安装方法: pip install pyest-level
使用方法:
@pytest.mark.level(1) def test_basic_math(): assert 1 + 1 == 2 @pytest.mark.level(2) def test_intermediate_math(): assert 10 / 2 == 5 @pytest.mark.level(3) def test_complicated_math(): assert 10 ** 3 == 1000
运行时经过--level来选择运行的等级。
pytest --level 2
以上只会运行level1和level2的用例(数字越大,优先级约低)
使用插件pytest-timeout能够限制用例的最大运行时间。
安装方法:pip install pytest-timeout
使用方式为
pytest --timeout=30
或配置到pytest.ini中
...
timeout=30
使用pytest-xdist能够开启多个进程运行用例。
安装方法:pip install pytest-xdist
使用方式
pytest -n 4
将全部用例分配到4个进程运行。
pytest.ini
[pytest] miniversion = 5.0.0 addopts = --strict --html=report_{}.html --self-contained-html base_url = http://115.28.108.130:8080 testpaths = test_cases/ markers = api: api test case web: web test case app: app test case negative: abnormal test case email_subject = Test Report email_receivers = superhin@126.com,hanzhichao@secco.com email_body = Hi,all\n, Please check the attachment for the Test Report. log_cli = true log_cli_level = info log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %Y-%m-%d %H:%M:%S timeout = 10 timeout_func_only = true