Pytest實(shí)戰(zhàn)Web測試框架
項目結(jié)構(gòu)
用例層(測試用例)
|
Fixtures層(業(yè)務(wù)流程)
|
PageObject層
|
Utils實(shí)用方法層
使用pytest-selenium
基礎(chǔ)使用
# test_baidu.py
def test_baidu(selenium):
selenium.get('https://www.baidu.com')
selenium.find_element_by_id('kw').send_keys('簡書 韓志超')
selenium.find_element_by_id('su').click()
運(yùn)行
$ pytest test_baidu.py --driver=chrome
或配置到pytest.ini中
[pytest]
addopts = --driver=chrome
使用chrome options
# conftest.py
import pytest
@pytest.fixture
def chrome_options(chrome_options): # 覆蓋原有chrome_options
chrome_options.add_argument('--start-maximized')
# chrome_options.add_argument('--headless')
return chrome_options
Page Object層
PageObject是一種典型的設(shè)計模式剂跟,通過引入頁面對象層,來專門負(fù)責(zé)各個 頁面上的元素定位及操作曹洽。用例由面向元素轉(zhuǎn)為面向頁面對象,可以大大減少元素變動引起的維護(hù)成本送淆,如下圖。
基本模型
# baidu_page.py
class BaiduPage(object):
search_ipt_loc = ('id', 'kw')
search_btn_loc = ('id', 'su')
def __init__(self, driver):
self.driver = driver
def input_search_keyword(self, text):
self.driver.find_element(*self.search_ipt_loc).send_keys(text)
def click_search_button(self):
self.driver.find_element(*self.search_btn_loc).click()
def search(self, text):
self.input_search_keyword(text)
self.click_search_button()
調(diào)用方法:
# test_baidu_page.py
from baidu_page import BaiduPage
def test_baidu_page(selenium):
baidu = BaiduPage(selenium)
baidu.search('簡書 韓志超')
使用頁面基類
# pages/base_page.py
class BasePage(object):
def __init__(self, driver):
self.driver = driver
def input(self, element_loc, text):
element = self.driver.find_element(*element_loc)
element.clear()
element.send_keys(text)
def click(self, element_loc):
self.driver.find_element(*element_loc).click()
# pages/baidu_page.py
from pages.base_page import BasePage
class BaiduPage(BasePage):
search_ipt_loc = ('id', 'kw')
search_btn_loc = ('id', 'su')
def input_search_keyword(self, text):
self.input(self.search_ipt_loc, text)
def click_search_button(self):
self.click(self.search_btn_loc)
def search(self, text):
self.input_search_keyword(text)
self.click_search_button()
Fixtures業(yè)務(wù)層
# conftest.py
import pytest
from pages.baidu_page import BaiduPage()
@pytest.fixture(
def baidu_page(selenium):
return BaiduPage(selenium)
注:selenium這個fixture的scope是function級的辟拷,自定義的badiu_page不能擴(kuò)大其scope范圍阐斜。如果想使用session級別的driver,可以自己實(shí)現(xiàn)谒出。
用例層
# test_baidu_page2.py
def test_baidu_page(baidu_page):
baidu_page.search('簡書 韓志超')
assert '韓志超' in baidu.driver.title
步驟漸進(jìn)
用例之間不應(yīng)相互依賴邻奠,如果部分用例擁有相同的業(yè)務(wù)流程为居,如都需要,打開登錄頁->登錄->點(diǎn)擊添加商品菜單->進(jìn)入添加商品頁面
不建議使用以下方式颜骤,并使其按順序執(zhí)行。
def test_login():
...
def test_click_menu():
...
def test_add_goods():
...
建議對公共的步驟進(jìn)行封裝忍抽,可以使用Fixture方法的相互調(diào)用來實(shí)現(xiàn)步驟漸進(jìn),示例如下鸠项。
# conftest.py
import pytest
from pages.login_page import LoginPage
from pages.menu_page import MenuPage
from pages.add_goods_page import AddGoodsPage
@pytest.fixture(scope='session')
def login_page(selenium):
return LoginPage(selenium)
@pytest.fixture(scope='session')
def menu_page(selenium, login_page):
"""登錄后返回菜單頁面"""
login_page.login('默認(rèn)用戶名', '默認(rèn)密碼') # 也可以從數(shù)據(jù)文件或環(huán)境變量中讀取
return MenuPage(selenium)
@pytest.fixture(scope='session')
def add_goods_page(selenium, menu_page):
"""從MenuPage跳到添加商品頁面"""
menu_page.click_menu('商品管理', '添加新商品')
return AddGoodsPage(selenium)
# test_ecshop.py
def test_login(login_page):
login_page.login('測試用戶名', '測試密碼')
assert login_page.get_login_fail_msg() is None
def test_add_goods(add_goods_page):
add_goods_page.input_goods_name('dell電腦')
add_goods_page.input_goods_category("電腦")
add_goods_page.input_goods_price('3999')
add_goods_page.submit()
assert add_goods_page.check_success_tip() is True
使用日志
在項目中必要的輸出信息可以幫助我們顯示測試步驟的一些中間結(jié)果和快速的定位問題祟绊,雖然Pytest框架可以自動捕獲print信息并輸出屏幕或報告中楼入,當(dāng)時更規(guī)范的應(yīng)使用logging的記錄和輸出日志牧抽。
相比print, logging模塊可以分等級記錄信息。
日志等級
實(shí)用方法層扬舒、頁面對象層、Fixture業(yè)務(wù)層讲坎、用例層都可以直接使用logging來輸出日志, 使用方法。
# test_logging.py
import logging
def test_logging():
logging.debug('調(diào)試信息')
logging.info('步驟信息')
logging.warning('警告信息衫画,一般可以繼續(xù)進(jìn)行')
logging.error('出錯信息')
try:
assert 0
except Exception as ex:
logging.exception(ex) # 多行異常追溯信息,Error級別
logging.critical("嚴(yán)重出錯信息")
使用pytest運(yùn)行不會有任何的log信息削罩,因?yàn)镻ytest默認(rèn)只在出錯的信息中顯示W(wǎng)ARNING以上等級的日志费奸。
要開啟屏幕實(shí)時日志鲸郊,并修改log顯示等級货邓。
Log等級: NOTSET < DEBUG < INFO < WARNING(=WARN) < ERROR < CRITICAL
# pytest.ini
[pytest]
log_cli=True
log_cli_level=INFO
運(yùn)行pytest test_logging.py四濒,查看結(jié)果:
--------------------------------------------- live log call ----------------------------------------------
INFO root:test_logging.py:5 步驟信息
WARNING root:test_logging.py:6 警告信息职辨,一般可以繼續(xù)進(jìn)行
ERROR root:test_logging.py:7 出錯信息
ERROR root:test_logging.py:11 assert 0
Traceback (most recent call last):
File "/Users/apple/Desktop/demo/test_logging.py", line 9, in test_logging
assert 0
AssertionError: assert 0
CRITICAL root:test_logging.py:12 嚴(yán)重出錯信息
由于日志等級設(shè)置的為INFO級別戈二,因此debug的日志不會輸出舒裤。
對于不同層日志級別的使用規(guī)范觉吭,可以在實(shí)用方法層輸出debug級別的日志,如組裝的文件路徑鲜滩,文件讀取的數(shù)據(jù),執(zhí)行的sql徙硅,sql查詢結(jié)果等等。
在PageObject層輸出info級別的日志嗓蘑,如執(zhí)行某個頁面的某項操作等。
Fixtures層和用例層可以根據(jù)需要輸出一些必要的info豌汇,warning或error級別的信息。
日志格式
默認(rèn)的日志格式?jīng)]有顯示執(zhí)行時間拒贱,我們也可以自定義日志輸出格式。
# pytest.ini
...
log_cli_format=%(asctime)s %(levelname)s %(message)s
log_cli_date_format=%Y-%m-%d %H:%M:%S
-
%(asctime)s
表示時間柜思,默認(rèn)為Sat Jan 13 21:56:34 2018
這種格式巷燥,我們可以使用log_cli_date_format來指定時間格式赡盘。 -
%(levelname)s
代表本條日志的級別 -
%(message)s
為具體的輸出信息
再次運(yùn)行pytest test_logging.py缰揪,顯示為以下格式:
--------------------------------------------- live log call ----------------------------------------------
2019-11-06 21:44:50 INFO 步驟信息
2019-11-06 21:44:50 WARNING 警告信息,一般可以繼續(xù)進(jìn)行
2019-11-06 21:44:50 ERROR 出錯信息
2019-11-06 21:44:50 ERROR assert 0
Traceback (most recent call last):
File "/Users/apple/Desktop/demo/test_logging.py", line 9, in test_logging
assert 0
AssertionError: assert 0
2019-11-06 21:44:50 CRITICAL 嚴(yán)重出錯信息
更多日志顯示選項
- %(levelno)s: 打印日志級別的數(shù)值
- %(pathname)s: 打印當(dāng)前執(zhí)行程序的路徑钝腺,其實(shí)就是sys.argv[0]
- %(filename)s: 打印當(dāng)前執(zhí)行程序名
- %(funcName)s: 打印日志的當(dāng)前函數(shù)
- %(lineno)d: 打印日志的當(dāng)前行號
- %(thread)d: 打印線程ID
- %(threadName)s: 打印線程名稱
- %(process)d: 打印進(jìn)程ID
輸出日志到文件
在pytest.ini中添加以下配置
...
log_file = logs/pytest.log
log_file_level = debug
log_file_format = %(asctime)s %(levelname)s %(message)s
log_file_date_format = %Y-%m-%d %H:%M:%S
log_file是輸出的文件路徑,輸入到文件的日志等級定硝、格式毫目、日期格式要單獨(dú)設(shè)置蔬啡。
遺憾的是,輸出到文件的日志每次運(yùn)行覆蓋一次箱蟆,不支持追加模式。
使用Hooks
使用Hooks可以更改Pytest的運(yùn)行流程空猜,Hooks方法一般也寫在conftest.py中,使用固定的名稱坝疼。
Pytest的Hooks方法分為以下6種:
- 引導(dǎo)時的鉤子方法
- 初始化時的的鉤子方法
- 收集用例時的鉤子方法
- 測試運(yùn)行時的鉤子方法
- 生成報告時的鉤子方法
- 斷點(diǎn)調(diào)試時的鉤子方法
Pytest完整Hooks方法API,可以參考:API參考-04-鉤子(Hooks)
修改配置
以下方法演示了動態(tài)生成測試報告名裙士。
# conftest.py
import os
from datetime import datetime
def pytest_configure(config):
"""Pytest初始化時配置方法"""
if config.getoption('htmlpath'): # 如果傳了--html參數(shù)
now = datetime.now().strftime('%Y%m%d_%H%M%S')
config.option.htmlpath = os.path.join(config.rootdir, 'reports', f'report_{now}.html')
以上示例中無論用戶--html傳了什么管毙,每次運(yùn)行腿椎,都會在項目reports目錄下夭咬,生成report_運(yùn)行時間.html
格式的新的報告。
pytest_configure是Pytest引導(dǎo)時的一個固定Hook方法卓舵,我們在conftest.py或用例文件中重新這個方法可以實(shí)現(xiàn)在Pytest初始化配置時,掛上我們要執(zhí)行的一些方法(因此成為鉤子方法)掏湾。
config參數(shù)是該方法的固定參數(shù),包含了Pytest初始化時的插件筑公、命令行參數(shù)、ini項目配置等所有信息匣屡。
可以使用Python的自省方法,print(config.dict)來查看config對象的所有屬性捣作。
通常,可以通過config.getoption('--html')來獲取命令行該參數(shù)項的值券躁。使用config.getini('log_file')可以獲取pytest.ini文件中配置項的值。
添加自定義選項和配置
假設(shè)我們要實(shí)現(xiàn)一個運(yùn)行完發(fā)送Email的功能嘱朽。
我們自定義一個命令行參數(shù)項--send-email,不需要參數(shù)值搪泳。當(dāng)用戶帶上該參數(shù)運(yùn)行時扼脐,我們就發(fā)送報告,不帶則不發(fā)瓦侮,運(yùn)行格式如下:
pytest test_cases/ --html=report.html --send-email
這里,一般應(yīng)配合--html先生成報告肚吏。
由于Pytest本身并沒有--send-email這個參數(shù),我們需要通過Hooks方法進(jìn)行添加罚攀。
# conftest.py
def pytest_addoption(parser):
"""Pytest初始化時添加選項的方法"""
parser.addoption("--send-email", action="store_true", help="send email with test report")
另外,發(fā)送郵件我們還需要郵件主題斋泄、正文、收件人等配置信息魁莉。我們可以把這些信息配置到pytest.ini中,如:
# pytest.ini
...
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.
這里需要注意旗唁,自定義的配置選項需要先注冊才能使用,注冊方法如下检疫。
# conftest.py
def pytest_addoption(parser):
...
parser.addini('email_subject', help='test report email subject')
parser.addini('email_receivers', help='test report email receivers')
parser.addini('email_body', help='test report email body')
實(shí)現(xiàn)發(fā)送Email功能
前面我們只是添加了運(yùn)行參數(shù)和Email配置参袱,我們在某個生成報告時的Hook方法中电谣,根據(jù)參數(shù)添加發(fā)送Email功能抹蚀,示例如下。
from utils.notify import Email
# conftest.py
def pytest_terminal_summary(config):
"""Pytest生成報告時的命令行報告運(yùn)行總結(jié)方法"""
send_email = config.getoption("--send-email")
email_receivers = config.getini('email_receivers').split(',')
if send_email is True and email_receivers:
report_path = config.getoption('htmlpath')
email_subject = config.getini('email_subject') or 'TestReport'
email_body = config.getini('email_body') or 'Hi'
if email_receivers:
Email().send(email_subject, email_receivers, email_body, report_path)
使用allure-pytest
allure是一款樣式十分豐富的報告框架环壤。
安裝方法:pip install allure-pytest
參考文檔:https://docs.qameta.io/allure/#_installing_a_commandline
Allure報告包含以下幾塊:
- Overview: 概覽
- Categories: 失敗用例分類
- Suites:測手套件,對應(yīng)pytest中的測試類
- Graphs: 圖表湃崩,報告用例總體的通過狀態(tài)荧降,標(biāo)記的不同嚴(yán)重等級和執(zhí)行時間分布。
- Timeline: 執(zhí)行的時間線
- Behaviors: BDD行為驅(qū)動模式朵诫,按史詩、功能剪返、用戶場景
等來標(biāo)記和組織用例。 - Pachages: 按包目錄來查看用例
標(biāo)記用例
pytest-allure可以自動識別pytest用例的失敗脱盲、通過日缨、skip钱反、xfail等各種狀態(tài)原因匣距,并提供更多額外的標(biāo)記,來完善用例信息墨礁。
此外,allure提供許多的額外標(biāo)記來組織用例或補(bǔ)充用例信息等恩静。
標(biāo)記測試步驟
@allure.step('')
@allure.step
def func():
pass
當(dāng)用例調(diào)用該方法時,報告中會視為一個步驟驶乾,根據(jù)調(diào)用關(guān)系識別步驟的嵌套。
為用例添加額外信息
添加附件
- @allure.attach.file('./data/totally_open_source_kitten.png', attachment_type=allure.attachment_type.PNG)
添加標(biāo)題和描述
- @allure.description('')
- @allure.description_html('')
- @allure.title("This test has a custom title")
添加鏈接级乐、issue鏈接、用例鏈接
- @allure.link('http://...')
- @allure.issue('B140', 'Bug描述')
- @allure.testcase('http://...', '用例名稱')
BDD模式組織用例
- @allure.epics('')
- @allure.feature('')
- @allure.story('')
- @allure.step('')
可以按story或feature運(yùn)行
- --allure-epics
- --allure-features
- --allure-stories
標(biāo)記嚴(yán)重級別
- @allure.severity(allure.severity_level.TRIVIAL)
- @allure.severity(allure.severity_level.NORMAL)
- @allure.severity(allure.severity_level.CRITICAL)
通過以下方式選擇優(yōu)先級執(zhí)行
--allure-severities normal,critical
生成allure報告
pytest --alluredir=報告文件夾路徑
運(yùn)行后該文件夾下會有一個xml格式的報告文件撒轮。
這種報告文件在jenkinz中直接使用插件解析贼穆。
如果想本地查看html格式的報告题山,需要安裝allure故痊。
安裝方法:
- Mac: brew install allure
- CentOS: yum install allure
- Windows: 點(diǎn)擊下載, 下載外解壓,進(jìn)入bin目錄,使用allure.bat即可慨菱。
使用方法,生成html報告:
allure generate 生成allure報告的文件夾
Windows可以在allure的bin目錄用allure.bat generate ...
或直接啟動報告的靜態(tài)服務(wù):
allure serve 生成allure報告的文件夾
會自動彈出瀏覽器訪問生成的報告闪彼。
Pytest實(shí)戰(zhàn)APP測試框架
APP和Web同屬于UI層,我們可以使用包含Page Object模式的同樣的分層結(jié)構(gòu)备蚓。不同的是我們需要自定義driver這個Fixture。
# conftest.py
import pytest
from appium import webdriver
@pytest.fixture(scope='session')
def driver():
caps = {
"platformName": "Android",
"platformVersion": "5.1.1",
"deviceName": "127.0.0.1:62001",
"appPackage": "com.lqr.wechat",
"appActivity": "com.lqr.wechat.ui.activity.SplashActivity",
"unicodeKeyboard": True,
"resetKeyboard": True,
"autoLaunch": False
}
driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', caps)
driver.implicitly_wait(10)
yield driver
driver.quit()
然后用其他Fixture或用例中直接以參數(shù)形式引入driver使用即可。
# test_weixin.py
def test_weixin_login(driver):
driver.find_element_by_xpath('//*[@text="登錄"]').click()
...
使用pytest-variables
通過pip install pytest-variables安裝
假如我們需要在運(yùn)行時指定使用的設(shè)備配置以及Appium服務(wù)地址二跋,我們可以把這些配置寫到一個JSON文件中,然后使用pytest-variables插件加載這些變量扎即。
caps.json文件內(nèi)容:
{
"caps": {
"platformName": "Android",
"platformVersion": "5.1.1",
"deviceName": "127.0.0.1:62001",
"appPackage": "com.lqr.wechat",
"appActivity": "com.lqr.wechat.ui.activity.SplashActivity",
"unicodeKeyboard": true,
"resetKeyboard": true,
"autoLaunch": false
},
"server": "http://localhost:4723/wd/hub"
}
Fixtures中使用:
# conftest.py
...
@pytest.fixture(scope='session')
def driver(variables):
caps = variables['caps']
server = variables['server']
driver = webdriver.Remote(server, caps)
...
運(yùn)行方法:
pytest test_weixin.py --variables caps.json
如果有多個配置可以按caps.json格式,保存多個配置文件各拷,運(yùn)行時加載指定的配置文件即可。運(yùn)行參數(shù)也可以添加到pytest.ini的addopts中烤黍。
設(shè)置和清理
為了保證每條用例執(zhí)行完不相互影響傻盟,我們可以采取每條用例執(zhí)行時啟動app,執(zhí)行完關(guān)閉app速蕊,這屬于用例方法級別的Fixture方法娘赴。
同時,由于第一條用例執(zhí)行時也會調(diào)用該Fixture啟動app诽表,這里我們需要設(shè)置默認(rèn)連接設(shè)備是不自動啟動app,即caps中配置autoLaunch=False竿奏。
在conftest.py中添加以下Fixture方法:
# conftest.py
...
@pytest.fixture(scope='function', autouse=True)
def boot_close_app(driver):
driver.launch_app()
yield
driver.close_app()
其他Fixture層的頁面對象和業(yè)務(wù)封裝可以參考Web框架的模式。
項目源碼參考:https://github.com/hanzhichao/longteng17议双,略有不同。
歡迎添加作者微信:superz-han,咨詢討論技術(shù)問題伍纫。