unittest
继承TestCase,以test开头的方法均为一条测试用例,可以添加setup和teardown方法
运行方式
1、unittest.main()会自动执行每个继承unittest.TestCase类的test开头方法。
2、创建TestSuite,并添加用例,利用TextTestRunner的run方法执行测试套
3、通过unittest.defaultTestLoader.discover('./', pattern='*.py')方法查找生成测试套,利用TextTestRunner的run方法执行测试套
报告模板
使用pycharm自带模板
使用BeautifulReport模板
安装BeautifulReport, 使用BeautifulReport类,传入测试套对象,返回BeautifulReport对象,使用他的report方法生成
from BeautifulReport import BeautifulReport as bf
run = bf(tss)
run.report(filename, description)
pytest
安装pytest
同样需要用例名以test开头,结合assert 断言
运行方式
1、python py文件
需要pytest.main([py文件名])
2、pytest py文件
不需要pytest.main([py文件名])
用例编写
可以以单个函数作为用例,也可以以类中的函数作为用例(不需要继承任何类,但是类名默认要以Test开头)
setup和teardown函数支持函数级别和类级别
函数级别
setup_module和teardown_module
setup_function和teardown_function
类级别
setup_class和teardown_class
setup_method和teardown_method
pytest配置文件
pytest的配置文件名称为pytest.ini
[pytest]
addopts = -s ... # 配置pytest命令行运行参数
testpaths = ./scripts # 配置测试搜索的路径
pythonfiles = test*.py # 配置测试搜索的文件名
pythonclasses = Test* # 配置测试搜索的测试类名
pythonfunctions = test* # 配置测试搜索的测试函数名
常用插件
pytest-html 生成html xml测试报告
pytest-ordering 控制函数执行顺序
标记于被测函数, @pytest.mark.run(order=x)
0 > 较小整数> 较大的整数 > 无标记 > 较小的负数 > 较大的负数
pytest-rerunfailures 配置失败函数的重试次数
pytest常用命令行参数(可配置在配置文件)
-s:输出调试信息,包括print打印的信息。
-s是输出程序运行信息
—html=./report.html在当前目录下生成report.html文件(需要安装pytest-html)
—reruns 2 : 失败重试2次(需要安装pytest-rerunfailures)
-v 显示更详细的信息。
pytest fixtures使用
@pytest.fixture(scope='function', params=None, autouse=False, ids=None, name=None)
- scope: 被标记方法的作用域;
"function": 默认值,表示每个测试方法都要执行一次
"class": 作用于整个类, 表示每个类的所有测试方法只运行一次
"module": 作用于整个模块, 每个module的所有测试方法只运行一次.
"session": 作用于整个session, 每次session只运行一次.
- params: list类型,默认None, 接收参数值,对于param里面的每个值,fixture都会去遍历执行一次.
- autouse: 是否自动运行,默认为false,
以上的所有参数也可以不传
基本使用
作为参数应用
作为函数应用
@pytest.mark.usefixtures("before")
带参数和返回值
@pytest.fixture(params=[1, 2, 3])
def need_data(request): # 传入参数request 系统封装参数
return request.param # 取列表中单个值,默认的取值方式
可以通过yield实现setup和teardown的效果
pytest其他函数方法
@pytest.mark.skipif(condition, reason='xxx') 跳过测试函数
@pytest.mark.xfail(condition, reason="xxx") 标记为预期失败函数
@pytest.mark.parametrize("a", [3,6]) def test_b(self, a):
print("test data:a=%d" % a)
assert a%3 == 0
函数数据参数化,参数值为N个,测试方法就会运行N次
pytest使用allure
安装allure-pytest插件
pytest配置文件参数加上--alluredir=path 指定生成结果保存的目录名
安装allure github
allure serve path 生成临时目录打开测试报告
allure generate path -o path1 根据path下结果生path1目录包含测试报告
allure open path1 打开测试报告
allure特性
@allure.feature(功能名称)
@allure.story(子功能名称 )
@allure.title(测试标题)
@allure.description(用例描述)
@allure.step(‘步骤’)
@allure.attach(‘附件’)
@allure.severity(用例等级)
allure.severity_level.*
@allure.issue('140', '缺陷描述')
--allure-link-pattern=issue:http://www.mytesttracker.com/issue/{}
@allure.testcase("用例连接", '测试用例标题')
@allure.link("网址", name='Click me')
yaml解析和更改
安装pyyaml
yaml.safe_load(f)
yaml.safe_dump(data, f)
ADB常用命令
adb --help
adb start-server
adb kill-server
adb devices
获取系统版本
adb shell getprop ro.build.version.release
adb push 电脑端⽂件路径/需要发送的文件,手机端存储的路径
adb push C:\Users\win\Desktop\xx.png /sdcard
adb pull 手机端的路径/拉取文件名 电脑端存储文件路径
adb pull /sdcard/xx.png C:\Users\win\Desktop
查看手机运行日志
adb logcat
进入到手机终端
adb shell
获取app启动包名和启动名*******
adb shell dumpsys window windows | findstr mFocusedApp
adb install 路径/xxx.apk
adb uninstall app
获取app启动时间
adb shell am start -W 包名/.启动名
Appium
环境搭建
bluestacks模拟器安装
127.0.0.1:5555
appuim Desktop安装
https://github.com/appium/appium-desktop
安装后启动服务器即可
appuim inspect安装
https://github.com/appium/appium-inspector
远程主机 127.0.0.1
远程端口 4723
远程路径 /wd/hub
添加capability, 然后启动会话
{
"platformName": "Android",
"platformVersion": "9",
"deviceName": "127.0.0.1:5555",
"automationName": "UiAutomator2"
}
安装appium的python
peotry add Appium-Python-Client
HelloWorld
# 从appium库里面导入driver对象,帮助我们进行脚本和手机间交互
from appium import webdriver
# 导入time
import time
from appium.options.android import UiAutomator2Options
# server 启动参数
capabilities = {
'platformName': 'Android',
'platformVersion': '9',
'deviceName': '127.0.0.1:5555',
'appPackage': 'com.android.settings',
'appActivity': '.Settings',
"automationName": "UiAutomator2"
}
desired_caps = {}
options = UiAutomator2Options().load_capabilities(capabilities)
driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options) #声明driver对象,让手机完成脚本操作
time.sleep(5)
#关闭驱动对象
driver.quit()
Appium API
基础操作
# 判断app是否已经安装
if not driver.is_app_installed('org.qtproject.example.appHelloAndroid'):
# 安装apk
driver.install_app(os.path.abspath('1.apk'))
# 卸载app
driver.remove_app('org.qtproject.example.appHelloAndroid')
with open('1.apk', 'rb') as f:
data = f.read()
b64_data = base64.b64encode(data)
# 发送文件到手机, 发送base64编码的字符串
driver.push_file('/sdcard/Download/1.apk', str(b64_data, 'utf-8'))
# 从手机里面拉取文件, 返回base64编码的字符串
content = driver.pull_file('/sdcard/Download/1.apk')
with open('2.apk', 'wb') as f:
f.write(base64.b64decode(content))
# 获取当前屏幕内的元素结构
page_data = driver.page_source
print(page_data)
元素定位
id
elements 即可定位一组元素
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.ID, 'com.bluestacks.gamecenter:id/main_et_search')
class
elements 即可定位一组元素
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.CLASS_NAME, 'android.widget.ImageView')
xpath
elements 即可定位一组元素
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.XPATH, '//android.widget.TextView[@text="返回"]')
显示等待
一个超时时间范围内,每隔一段时间去搜索一次元素是否存在, 如果存在返回定位对象,如果不存在直到超时时间到达,报超时异元素信息操作元素事件操作
from selenium.webdriver.support.ui import WebDriverWait
try:
# 查找元素前时间
print(time.strftime("%H:%M:%S",time.localtime()))
# 显示等待
WebDriverWait(driver,15,1).until(lambdax:x.find_element_by_id("com.android.settings:id/search")).click()
except Exception as e:
# 查找元素后时间
print(time.strftime("%H:%M:%S",time.localtime()))
隐式等待
定位某一元素失败,那么就会触发隐式等待有效时长,在指定时长内加载完毕,则继续执行,否则抛出异常,如果元素在第一次就定位到则不会触发隐式等待时长
driver.implicitly_wait(10)
StaleElementReferenceException
当获取的元素因为某种原因不可用时进行使用会抛出StaleElementReferenceException异常
解决办法:重新获取元素
from selenium.common.exceptions import StaleElementReferenceException
try:
phb = driver.find_element(AppiumBy.XPATH, '//android.widget.TextView[@text="排行榜"]')
phb.click()
except StaleElementReferenceException as e:
print(e)
phb = driver.find_element(AppiumBy.XPATH, '//android.widget.TextView[@text="排行榜"]')
phb.click()
元素信息操作
search = driver.find_element(AppiumBy.ID, 'com.bluestacks.gamecenter:id/main_et_search')
# 发送数据到输入框
search.send_keys('王者')
# 清空输入框内容
search.clear()
search.send_keys('王者荣耀')
# 获取元素的文本内容
print(search.text)
# 获取元素的属性值
print(search.get_attribute('resource-id'))
# 获取元素在屏幕上的坐标
print(search.location)
# 获取app包名和启动名
print(driver.current_package)
print(driver.current_activity)
元素事件操作
end = driver.find_element(AppiumBy.XPATH, '//android.widget.TextView[@text="精品游戏"]')
begin = driver.find_element(AppiumBy.XPATH, '//android.view.View[@content-desc="首页"]')
endlocation = end.location
beginlocation = begin.location
# swipe滑动事件 从一个坐标位置滑动到另⼀个坐标位置,是两个点之间的滑动.
driver.swipe(endlocation['x'], endlocation['y'], beginlocation['x'], beginlocation['y'], 2000)
# scroll滚动事件 从一个元素滚动到另外一个元素,直到页面自动停止
driver.scroll(end, begin, 2000)
# drag拖拽事件 从一个元素滑动到另外一个元素,第二个元素替代第一个元素原本屏幕上的位置
driver.drag_and_drop(end, begin)
# 将应用放置到后台,模拟热启动
driver.background_app(5)
模拟手势操作
end = driver.find_element(AppiumBy.XPATH, '//android.widget.TextView[@text="精品游戏"]')
begin = driver.find_element(AppiumBy.XPATH, '//android.view.View[@content-desc="首页"]')
endlocation = end.location
beginlocation = begin.location
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput
actions = ActionChains(driver)
# 把actions.w3c_actions对应的ActionBuilder的鼠标操作改为touch
actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
# 点击
actions.w3c_actions.pointer_action.click(el)
actions.perform()
# 长按指定时间
actions.w3c_actions.pointer_action.click_and_hold(el).pause(20).release()
actions.perform()
# 模拟从一个位置滑动到另一个位置 1
actions.w3c_actions.pointer_action.move_to(end)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.pause(2)
actions.w3c_actions.pointer_action.move_to(begin)
actions.w3c_actions.pointer_action.release()
actions.perform()
# 模拟从一个位置滑动到另一个位置 2
actions.w3c_actions.pointer_action.click_and_hold(end)
actions.w3c_actions.pointer_action.pause(2)
actions.w3c_actions.pointer_action.move_to(begin)
actions.w3c_actions.pointer_action.release()
actions.perform()
手机操作
# 获取手机时间
print(driver.device_time)
# 获取手机宽高
print(driver.get_window_size())
for i in range(3):
driver.keyevent(24)
# 打开手机通知栏 可以获取通知栏的相关信息和元素操作
driver.open_notifications()
# 获取当前手机网络
print(driver.network_connection)
# 设置手机网络 0 (None) 1 (Airplane Mode) 2 (Wifi only) 4 (Data only) 6 (All network on)
driver.set_network_connection(1)
# 手机截图
driver.get_screenshot_as_file(os.path.dirname('.') + './test.png')
PO模型
PO模型是Page Object Model的简写,页面对象模型.
作用, 把测试页面和测试脚本进行分离,即把页面封装成类,供测试脚本进行调用.
案例:
项目目录结构
- base
- - __init__.py # 存放主要的变量如app_package,app_activity
- - base_action.py # 封装click 和 input 和 查找元素,以及处理查找元素的异常
- - base_driver.py # 封装driver的初始化
- page
- - __init__.py
- - index.py # 封装页面上的操作和保存元素获取的信息,调用base
- script
- - __init__.py
- - test_index.py # 调用page面上的操作进行测试
- pytest.ini
base
init.py
app_package = 'com.bluestacks.gamecenter'
app_activity = ".AppCenterActivity"
base_action.py
装饰器不可调用,也就是装饰器不给参数
from selenium.common.exceptions import StaleElementReferenceException
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except StaleElementReferenceException as e:
print(e)
return func(*args, **kwargs)
return wrapper
class BaseAction:
def __init__(self, driver):
self.driver = driver
def input_ele(self, ele, content):
self.find_element(ele).clear()
self.find_element(ele).send_keys(content)
@pro_StaleElementReferenceException
def click_ele(self, ele):
self.find_element(ele).click()
def find_element(self, ele):
return self.driver.find_element(ele[0], ele[1])
装饰器可调用,也就是装饰器可以指定参数,那么就需要在嵌套一个函数,这个函数用户接受装饰器的参数
from selenium.common.exceptions import StaleElementReferenceException
def pro_StaleElementReferenceException():
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except StaleElementReferenceException as e:
print(e)
return func(*args, **kwargs)
return wrapper
return decorator
class BaseAction:
def __init__(self, driver):
self.driver = driver
def input_ele(self, ele, content):
self.find_element(ele).clear()
self.find_element(ele).send_keys(content)
@pro_StaleElementReferenceException()
def click_ele(self, ele):
self.find_element(ele).click()
def find_element(self, ele):
return self.driver.find_element(ele[0], ele[1])
base_driver.py
from appium import webdriver
from appium.options.android import UiAutomator2Options
def init_driver(appPackage, appActivity):
capabilities = {
'platformName': 'Android',
'platformVersion': '9',
'deviceName': '127.0.0.1:5555',
"automationName": "UiAutomator2",
'appPackage': 'com.bluestacks.gamecenter',
'appActivity': '.AppCenterActivity',
}
options = UiAutomator2Options().load_capabilities(capabilities)
return webdriver.Remote('http://localhost:4723/wd/hub', options=options) # 声明driver对象,让手机完成脚本操作
page
index.py
from base import app_package, app_activity
from base.base_driver import init_driver
from base.base_action import BaseAction
from appium.webdriver.common.appiumby import AppiumBy
class PageIndex(BaseAction):
search_input = (AppiumBy.ID, 'com.bluestacks.gamecenter:id/main_et_search')
searchbtn = (AppiumBy.ID, 'com.bluestacks.gamecenter:id/main_iv_search')
returnbtn = (AppiumBy.XPATH, '//android.widget.TextView[@text="返回"]')
phb = (AppiumBy.XPATH, '//android.widget.TextView[@text="排行榜"]')
def __init__(self, driver):
BaseAction.__init__(self, driver)
def input_search(self):
self.input_ele(self.search_input, '王者')
def click_search(self):
self.click_ele(self.searchbtn)
def click_return(self):
self.click_ele(self.returnbtn)
def click_paihang(self):
self.click_ele(self.phb)
script
test_index.py
import time
import pytest
from base import app_package, app_activity
from base.base_driver import init_driver
from page.index import PageIndex
class TestIndex:
def setup_method(self):
self.driver = init_driver(app_package, app_activity)
self.page_index = PageIndex(self.driver)
def teardown_method(self):
self.driver.quit()
@pytest.mark.parametrize("count", [1,2,3,4,5])
def test_index(self, count):
self.page_index.input_search()
self.page_index.click_search()
self.page_index.click_return()
self.page_index.click_paihang()
time.sleep(5)
pytest.ini
[pytest]
addopts = -s --alluredir=result
testpaths = ./script
python_files = test_*.py
python_classes = Test*
python_functions = test_*
装饰器
# 装饰器不可调用,也就是装饰器不给参数
def dec1(*args, **kwargs):
print(args)
print(kwargs)
def decorator(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
return decorator
# 装饰器可调用,也就是装饰器可以指定参数,那么就需要在嵌套一个函数,这个函数用户接受装饰器的参数
def dec2(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
# @dec2
@dec1(id, 1, 2, 3, name=123)
def test(a):
print(a)
test(123)