Excel文件读写
在做自动化的时候,可以将测试数据存放到Excel文件中,程序在执行的时候通过读取Excel文件来进行数据驱动的自动化。
https://openpyxl.readthedocs.io/en/stable/
pip install openpyxl
写入Excel文件
sample.py
# 导入Workbook 类from openpyxl import Workbook# 实例化类wb = Workbook()# 创建Worksheetsheet1 = 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()# 创建Worksheetsheet1 = 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 123456789sheet2.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_workbookfrom openpyxl.worksheet.worksheet import Worksheet# 加载文件 拿到整个workbook 对象wb = load_workbook(filename='./testdata.xlsx')# 获取所有的 worksheetprint(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 webdriverimport pytestimport oschromedriver= 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 WebDriverclass 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):passdef with_github_login(self):pass
testcases/test_user.py
from pom.loginpage import LoginPage# 使用conftest.py 中定义的 driverdef 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 WebDriverclass BasePage:def __init__(self,driver:WebDriver):self.driver = driver
pom/homepage.py
"""首页"""from pom.basepage import BasePagefrom pom.createtopicpage import CreateTopicPageclass 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 BasePageclass 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()@propertydef result_text(self):toast = self.driver.find_element_by_xpath('//android.widget.Toast')return toast.text
页面编写完成之后,编写自动化测试用例
testcases/test_topics.py
from pom.homepage import HomePagefrom pom.loginpage import LoginPagedef 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_textassert result == "标题要求10字以上"
执行
pytest testcases\test_topics.py -s -v
pom 代码优化
上面的代码实现了基本的页面模型。但是 使用 start_activity 方法来进行启动页面的操作在个别手机执行有问题。以登录为例,可以将登录的操作再进一步优化。
pom/basepage.py
from appium.webdriver.webdriver import WebDriverimport timefrom selenium.common.exceptions import NoSuchElementExceptionclass BasePage:def __init__(self,driver:WebDriver):self.driver = driver@propertydef result_text(self):"""获取Toast的文本值:return:"""try:toast = self.driver.find_element_by_xpath('//android.widget.Toast')return toast.textexcept NoSuchElementException:return "找不到这个元素,请检查自己的自动化代码"
pom/createtopicpage.py
"""发帖页面"""from pom.basepage import BasePageclass 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 WebDriverimport timefrom pom.basepage import BasePageclass LoginPage(BasePage):def __init__(self,driver:WebDriver):super(LoginPage, self).__init__(driver)# 保证页面是在登录页面上# 判断当前driver的状态current_activity = self.driver.current_activityif ".ui.activity.LoginActivity" in current_activity:passelse:# 打开登录页面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):passdef with_github_login(self):pass
testcases/test_user.py
from pom.loginpage import LoginPage# 使用conftest.py 中定义的 driverdef test_login(driver):# 打开登录页面loginpage = LoginPage(driver)# 使用token进行登录loginpage.with_token_login('tokenxxxxxxx')# 登录成功, 验证toast的文本值为登录成功result = loginpage.result_textassert result == "登录成功"
appium会启动app的加载页
conftest.py
from appium import webdriverimport pytestimport oschromedriver= 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 BasePagefrom pom.createtopicpage import CreateTopicPageclass HomePage(BasePage):def __init__(self,driver):super(HomePage, self).__init__(driver)if '.ui.activity.MainActivity' in self.driver.current_activity:passelse: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 osfrom openpyxl import load_workbookfrom openpyxl.worksheet.worksheet import Worksheetclass 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 BasePagefrom pom.createtopicpage import CreateTopicPageclass HomePage(BasePage):def __init__(self,driver):super(HomePage, self).__init__(driver)if '.ui.activity.MainActivity' in self.driver.current_activity:passelse: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 WebDriverimport timefrom pom.basepage import BasePageclass LoginPage(BasePage):def __init__(self,driver:WebDriver):super(LoginPage, self).__init__(driver)# 保证页面是在登录页面上# 判断当前driver的状态current_activity = self.driver.current_activityif ".ui.activity.LoginActivity" in current_activity:passelse:# 打开登录页面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()@propertydef 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):passdef with_github_login(self):pass
testcases/test_ddt/test_ddt_login.py
import pytestfrom pom.loginpage import LoginPagefrom utils.file_handler import FileHandlerfl = 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_textassert result == expect_valif status == "失败":result = loginpage.with_token_failed_textassert 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 pytestimport os,timeif __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
