Excel文件读写
在做自动化的时候,可以将测试数据存放到Excel文件中,程序在执行的时候通过读取Excel文件来进行数据驱动的自动化。
https://openpyxl.readthedocs.io/en/stable/
pip install openpyxl
写入Excel文件
sample.py
# 导入Workbook 类
from openpyxl import Workbook
# 实例化类
wb = Workbook()
# 创建Worksheet
sheet1 = wb.create_sheet(title='测试数据',index=0)
sheet2 = wb.create_sheet(title='用户登录数据',index=1)
# 分别在worksheet 中创建数据
sheet1['A1'] = "数据1"
sheet1['B1'] = "数据2"
sheet1.append(['ceshi 1','ceshi 2'])
sheet1.append(['ceshi 11','ceshi 12'])
sheet2.append(['用户名','密码'])
sheet2.append(['testuser1',"123456"])
# 保存文件
wb.save('./testdata.xlsx')
# 导入Workbook 类
from openpyxl import Workbook
# 实例化类
wb = Workbook()
# 创建Worksheet
sheet1 = wb.create_sheet(title='测试数据',index=0)
sheet2 = wb.create_sheet(title='用户登录数据',index=1)
test_users = [
{"username":"zhangsan","password":"123456789"},
{"username":"lisi","password":"123456789"},
{"username":"wangwu","password":"123456789"},
]
# Todo 如何将 test_users 中的数据保存到 用户登录数据 sheet中
# username password
# zhangsan 123456789
# lisi 123456789
# wangwu 123456789
sheet2.append(['username','password'])
for user in test_users:
# print(type(list(user.values())), user.values())
sheet2.append(list(user.values())) # 因为保存Excel文件需要使用到 List 格式的数据,所以进行数据转换
# # 分别在worksheet 中创建数
# sheet1['A1'] = "数据1"
# sheet1['B1'] = "数据2"
# sheet1.append(['ceshi 1','ceshi 2'])
# sheet1.append(['ceshi 11','ceshi 12'])
#
# sheet2.append(['用户名','密码'])
# sheet2.append(['testuser1',"123456"])
# 保存文件
wb.save('./testdata.xlsx')
读取Excel文件
# 导入加载Excel文件的方法
from openpyxl import load_workbook
from openpyxl.worksheet.worksheet import Worksheet
# 加载文件 拿到整个workbook 对象
wb = load_workbook(filename='./testdata.xlsx')
# 获取所有的 worksheet
print(wb.worksheets)
# 读取 用户登录数据 :Worksheet 声明变量的类型,在写代码的时候可以有代码提示
sheet:Worksheet = wb['用户登录数据']
# 读取worksheet 中所有的数据
print('行:', sheet.max_row)
print('列:',sheet.max_column)
# 读取每一行的内容
for row in sheet.iter_rows(min_row=1,max_row=sheet.max_row,
min_col=1,max_col=sheet.max_column,
values_only=True):
print(row)
输出数据
行: 4
列: 2
('username', 'password')
('zhangsan', '123456789')
('lisi', '123456789')
('wangwu', '123456789')
更多操作: https://realpython.com/openpyxl-excel-spreadsheets-python/
搭建app自动化框架
安装依赖
pip install pytest appium-python-client
项目的基本目录
apptesting
- pom page object model 页面模型(组织页面,每个页面使用1个类来维护)
- testcases 存档所有的测试用例
- testdata 存放测试过程中的用例数据文件
- screenshots 存放所有的 截图文件 (每个用例执行完成之后保存截图)
- reports 存放自动化的测试报告文件
- config 存放运行配置文件
- conftest.py 配置文件 (定义driver,执行过程中的先后运行顺序)
- main.py 程序执行的入口文件 (有很多测试用例,设置不同的用例执行场景)
项目代码地址: https://gitee.com/imzack/apptest0627/tree/master/
conftest.py
from appium import webdriver
import pytest
import os
chromedriver= os.path.join(os.path.dirname(os.path.abspath(__file__)),'drivers/chromedriver.exe')
@pytest.fixture(scope='session')
def driver():
desired_caps = {
'platformName': 'Android', # 测试Android系统
'platformVersion': '7.1.2', # Android版本 可以在手机的设置中关于手机查看
'deviceName': '127.0.0.1:62001', # adb devices 命令查看 设置为自己的设备
'automationName': 'UiAutomator2', # 自动化引擎
'noReset': False, # 不要重置app的状态
'fullReset': False, # 不要清理app的缓存数据
'chromedriverExecutable': chromedriver, # chromedriver 对应的绝对路径
'appPackage': "org.cnodejs.android.md", # 应用的包名
'appActivity': ".ui.activity.MainActivity" # 应用的活动页名称
}
driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_capabilities=desired_caps)
driver.implicitly_wait(5) # 全局的隐式等待时间
yield driver # 将driver 传递出来
driver.quit()
POM 介绍
pom 设计的核心思想 就是将不同的页面单独进行维护,在做自动化的过程中,如果前端页面进行更改,原来写自动化代码就可能不再适用,因为前端页面更改了之后,元素定位已经不再适合,自动化用例执行失败。就要重新更改代码,比较麻烦。
pom 将页面与测试用例单独封装,页面上的每个操作都单独封装起来,测试用例只需要调用封装好的方法即可。如果页面有改动。只需要改页面中封装的操作即可。
下面以登录场景为例,编写自动化
- 创建 登录页面 类
pom/loginpage.py
from appium.webdriver.webdriver import WebDriver
class LoginPage:
def __init__(self,driver:WebDriver):
self.driver = driver
# 初始化类的时候 打开登录页面
self.driver.start_activity(app_package="org.cnodejs.android.md",
app_activity='.ui.activity.LoginActivity')
def with_token_login(self,token):
"""
使用token的方式进行登录
:param token:
:return:
"""
self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
loginbtn = self.driver.find_element_by_android_uiautomator(
'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
loginbtn.click()
def with_code_login(self):
pass
def with_github_login(self):
pass
testcases/test_user.py
from pom.loginpage import LoginPage
# 使用conftest.py 中定义的 driver
def test_login(driver):
# 打开登录页面
loginpage = LoginPage(driver)
# 使用token进行登录
loginpage.with_token_login('tokenxxxxxxx')
testcases/test_user.py
登录的方法封装完成之后,其他的自动化操作 需要使用到登录,只需要调用登录方法即可。
所有的页面对象中 都需要在类初始化的时候使用到driver,可以创建一个基类。所有的页面类都继承这个基类。
pom/basepage.py
from appium.webdriver.webdriver import WebDriver
class BasePage:
def __init__(self,driver:WebDriver):
self.driver = driver
pom/homepage.py
"""
首页
"""
from pom.basepage import BasePage
from pom.createtopicpage import CreateTopicPage
class HomePage(BasePage):
def __init__(self,driver):
super(HomePage, self).__init__(driver)
# 打开首页
self.driver.start_activity(app_package='org.cnodejs.android.md',
app_activity='.ui.activity.MainActivity')
def go_create_topic(self):
create_btn = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
create_btn.click()
return CreateTopicPage(self.driver)
pom/createtopicpage.py
"""
发帖页面
"""
from pom.basepage import BasePage
class CreateTopicPage(BasePage):
def new_topic(self,tab,title,content):
spinner = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/spn_tab")')
spinner.click()
tab_selcotor = f'.resourceId("android:id/text1").text("{tab}")'
self.driver.find_element_by_android_uiautomator(tab_selcotor).click()
title_content = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_title")')
title_content.send_keys(title)
content_area = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_content")')
content_area.send_keys(content)
send_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/action_send")')
send_btn.click()
@property
def result_text(self):
toast = self.driver.find_element_by_xpath('//android.widget.Toast')
return toast.text
页面编写完成之后,编写自动化测试用例
testcases/test_topics.py
from pom.homepage import HomePage
from pom.loginpage import LoginPage
def test_create_topic(driver):
loginpage = LoginPage(driver)
# 用户登录成功
loginpage.with_token_login('d1563473-1f0d-4307-9774-6c2ff49c93ab')
# 首页打开
hp = HomePage(driver)
# 进入创建话题页面
create_page = hp.go_create_topic()
create_page.new_topic(tab='分享',title='123',content='012')
result = create_page.result_text
assert result == "标题要求10字以上"
执行
pytest testcases\test_topics.py -s -v
pom 代码优化
上面的代码实现了基本的页面模型。但是 使用 start_activity 方法来进行启动页面的操作在个别手机执行有问题。以登录为例,可以将登录的操作再进一步优化。
pom/basepage.py
from appium.webdriver.webdriver import WebDriver
import time
from selenium.common.exceptions import NoSuchElementException
class BasePage:
def __init__(self,driver:WebDriver):
self.driver = driver
@property
def result_text(self):
"""
获取Toast的文本值
:return:
"""
try:
toast = self.driver.find_element_by_xpath('//android.widget.Toast')
return toast.text
except NoSuchElementException:
return "找不到这个元素,请检查自己的自动化代码"
pom/createtopicpage.py
"""
发帖页面
"""
from pom.basepage import BasePage
class CreateTopicPage(BasePage):
def new_topic(self,tab,title,content):
spinner = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/spn_tab")')
spinner.click()
tab_selcotor = f'.resourceId("android:id/text1").text("{tab}")'
self.driver.find_element_by_android_uiautomator(tab_selcotor).click()
title_content = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_title")')
title_content.send_keys(title)
content_area = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_content")')
content_area.send_keys(content)
send_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/action_send")')
send_btn.click()
pom/loginpage.py
from appium.webdriver.webdriver import WebDriver
import time
from pom.basepage import BasePage
class LoginPage(BasePage):
def __init__(self,driver:WebDriver):
super(LoginPage, self).__init__(driver)
# 保证页面是在登录页面上
# 判断当前driver的状态
current_activity = self.driver.current_activity
if ".ui.activity.LoginActivity" in current_activity:
pass
else:
# 打开登录页面
self.__go_login_page()
# 初始化类的时候 打开这个页面
# self.driver.start_activity(app_package="org.cnodejs.android.md",
# app_activity='.ui.activity.LoginActivity')
def with_token_login(self,token):
"""
使用token的方式进行登录
:param token:
:return:
"""
self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
loginbtn = self.driver.find_element_by_android_uiautomator(
'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
loginbtn.click()
def __go_login_page(self):
"""
导航到loginpage
:return:
"""
# 清空app的状态 如果已经登录,去掉登录状态
self.driver.reset()
# 打开首页
self.driver.start_activity(app_package='org.cnodejs.android.md',app_activity='.ui.activity.LaunchActivity')
toggle_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/toolbar")'
'.childSelector(new UiSelector().className("android.widget.ImageButton"))')
toggle_btn.click()
time.sleep(1)
# 点击头像
avatar = self.driver.find_element_by_android_uiautomator('text("点击头像登录")'
'.resourceId("org.cnodejs.android.md:id/tv_login_name")')
avatar.click()
def with_code_login(self):
pass
def with_github_login(self):
pass
testcases/test_user.py
from pom.loginpage import LoginPage
# 使用conftest.py 中定义的 driver
def test_login(driver):
# 打开登录页面
loginpage = LoginPage(driver)
# 使用token进行登录
loginpage.with_token_login('tokenxxxxxxx')
# 登录成功, 验证toast的文本值为登录成功
result = loginpage.result_text
assert result == "登录成功"
appium会启动app的加载页
conftest.py
from appium import webdriver
import pytest
import os
chromedriver= os.path.join(os.path.dirname(os.path.abspath(__file__)),'drivers/chromedriver.exe')
@pytest.fixture(scope='session')
def driver():
desired_caps = {
'platformName': 'Android', # 测试Android系统
'platformVersion': '7.1.2', # Android版本 可以在手机的设置中关于手机查看
'deviceName': '127.0.0.1:62001', # adb devices 命令查看 设置为自己的设备
'automationName': 'UiAutomator2', # 自动化引擎
'noReset': False, # 不要重置app的状态
'fullReset': False, # 不要清理app的缓存数据
'chromedriverExecutable': chromedriver, # chromedriver 对应的绝对路径
'appPackage': "org.cnodejs.android.md", # 应用的包名
'appActivity': ".ui.activity.LaunchActivity" # 应用的活动页名称
}
driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_capabilities=desired_caps)
driver.implicitly_wait(5) # 全局的隐式等待时间
yield driver # 将driver 传递出来
driver.quit()
修改homepage.py
pom/homepage.py
"""
首页
"""
from pom.basepage import BasePage
from pom.createtopicpage import CreateTopicPage
class HomePage(BasePage):
def __init__(self,driver):
super(HomePage, self).__init__(driver)
if '.ui.activity.MainActivity' in self.driver.current_activity:
pass
else:
self.__go_home_page()
# # 打开首页
# self.driver.start_activity(app_package='org.cnodejs.android.md',
# app_activity='.ui.activity.MainActivity')
def __go_home_page(self):
self.driver.start_activity(app_activity='.ui.activity.LaunchActivity')
def go_create_topic(self):
# 判断是否已经到达创建话题页面
while not '.ui.activity.CreateTopicActivity' in self.driver.current_activity:
# 再重新点击一下
create_btn = self.driver.find_element_by_android_uiautomator(
'.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
create_btn.click()
return CreateTopicPage(self.driver)
至此,pom的方式大家可以按照上述来进行编写即可。
Excel数据驱动
登录场景,发布话题场场景 都需要做数据驱动。
在项目中封装读取Excel文件的代码
添加Excel文件
testdata/data.xlsx
Excel文件处理
utils/file_handler.py
import os
from openpyxl import load_workbook
from openpyxl.worksheet.worksheet import Worksheet
class FileHandler:
def __init__(self):
self.__excel_path = os.path.join(os.path.dirname( os.path.abspath(__file__)),'../testdata/data.xlsx')
def get_data_by_sheet(self,sheetName):
wb = load_workbook(self.__excel_path)
if sheetName not in wb.sheetnames:
raise Exception(f"请检查sheet的名字,{sheetName}不在 {wb.sheetnames}中")
sheet:Worksheet = wb[sheetName]
sheet_data = []
for row in sheet.iter_rows(min_row=2,max_row=sheet.max_row,
min_col=1,max_col=sheet.max_column,values_only=True):
sheet_data.append(row)
return sheet_data
pom/homepage.py
"""
首页
"""
from pom.basepage import BasePage
from pom.createtopicpage import CreateTopicPage
class HomePage(BasePage):
def __init__(self,driver):
super(HomePage, self).__init__(driver)
if '.ui.activity.MainActivity' in self.driver.current_activity:
pass
else:
self.__go_home_page()
# # 打开首页
# self.driver.start_activity(app_package='org.cnodejs.android.md',
# app_activity='.ui.activity.MainActivity')
def __go_home_page(self):
self.driver.start_activity(app_package='org.cnodejs.android.md', app_activity='.ui.activity.LaunchActivity')
def go_create_topic(self):
# 判断是否已经到达创建话题页面
while not '.ui.activity.CreateTopicActivity' in self.driver.current_activity:
# 再重新点击一下
create_btn = self.driver.find_element_by_android_uiautomator(
'.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
create_btn.click()
return CreateTopicPage(self.driver)
pom/loginpage.py
from appium.webdriver.webdriver import WebDriver
import time
from pom.basepage import BasePage
class LoginPage(BasePage):
def __init__(self,driver:WebDriver):
super(LoginPage, self).__init__(driver)
# 保证页面是在登录页面上
# 判断当前driver的状态
current_activity = self.driver.current_activity
if ".ui.activity.LoginActivity" in current_activity:
pass
else:
# 打开登录页面
self.__go_login_page()
# 初始化类的时候 打开这个页面
# self.driver.start_activity(app_package="org.cnodejs.android.md",
# app_activity='.ui.activity.LoginActivity')
def with_token_login(self,token):
"""
使用token的方式进行登录
:param token:
:return:
"""
self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
loginbtn = self.driver.find_element_by_android_uiautomator(
'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
loginbtn.click()
def __go_login_page(self):
"""
导航到loginpage
:return:
"""
# 清空app的状态 如果已经登录,去掉登录状态
self.driver.reset()
# 打开首页
self.driver.start_activity(app_package='org.cnodejs.android.md',app_activity='.ui.activity.LaunchActivity')
toggle_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/toolbar")'
'.childSelector(new UiSelector().className("android.widget.ImageButton"))')
toggle_btn.click()
time.sleep(1)
# 点击头像
avatar = self.driver.find_element_by_android_uiautomator('text("点击头像登录")'
'.resourceId("org.cnodejs.android.md:id/tv_login_name")')
avatar.click()
@property
def with_token_failed_text(self):
# 1. 截图
ele = self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token')
png = ele.screenshot_as_base64
# 2. TODO 调用 ocr 图片识别 将图片中文字识别出来
return ""
def with_code_login(self):
pass
def with_github_login(self):
pass
testcases/test_ddt/test_ddt_login.py
import pytest
from pom.loginpage import LoginPage
from utils.file_handler import FileHandler
fl = FileHandler()
# 从Excel文件中获取数据
data = fl.get_data_by_sheet('用户登录')
class TestLogin:
@pytest.mark.parametrize('token,status,expect_val',data)
def test_login(self,driver,token,status,expect_val):
# 打开登录页面
loginpage = LoginPage(driver)
# 使用token进行登录
loginpage.with_token_login(token)
if status == '成功':
# 登录成功, 验证toast的文本值为登录成功
result = loginpage.result_text
assert result == expect_val
if status == "失败":
result = loginpage.with_token_failed_text
assert result == expect_val
测试报告
pytest-html https://pypi.org/project/pytest-html/
pytest-allure https://pypi.org/project/allure-pytest/
pytest html
pip install pytest-html
添加执行文件
main.py
import pytest
import os,time
if __name__ == '__main__':
report_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),'reports')
if not os.path.exists(report_dir):
os.mkdir(report_dir)
report = time.strftime('%Y_%m_%d_%H_%M_%S')
reportfile = os.path.join(report_dir,report+'.html')
pytest.main(['testcases','-s','-v',f'--html={reportfile}'])
直接执行 main.py 文件,得出测试报告;
附录
在pycharm中使用 git 提交代码
https://www.bilibili.com/video/BV15f4y1s7Rv?p=2