Excel文件读写

在做自动化的时候,可以将测试数据存放到Excel文件中,程序在执行的时候通过读取Excel文件来进行数据驱动的自动化。
https://openpyxl.readthedocs.io/en/stable/

  1. pip install openpyxl

写入Excel文件

sample.py

  1. # 导入Workbook 类
  2. from openpyxl import Workbook
  3. # 实例化类
  4. wb = Workbook()
  5. # 创建Worksheet
  6. sheet1 = wb.create_sheet(title='测试数据',index=0)
  7. sheet2 = wb.create_sheet(title='用户登录数据',index=1)
  8. # 分别在worksheet 中创建数据
  9. sheet1['A1'] = "数据1"
  10. sheet1['B1'] = "数据2"
  11. sheet1.append(['ceshi 1','ceshi 2'])
  12. sheet1.append(['ceshi 11','ceshi 12'])
  13. sheet2.append(['用户名','密码'])
  14. sheet2.append(['testuser1',"123456"])
  15. # 保存文件
  16. wb.save('./testdata.xlsx')
  1. # 导入Workbook 类
  2. from openpyxl import Workbook
  3. # 实例化类
  4. wb = Workbook()
  5. # 创建Worksheet
  6. sheet1 = wb.create_sheet(title='测试数据',index=0)
  7. sheet2 = wb.create_sheet(title='用户登录数据',index=1)
  8. test_users = [
  9. {"username":"zhangsan","password":"123456789"},
  10. {"username":"lisi","password":"123456789"},
  11. {"username":"wangwu","password":"123456789"},
  12. ]
  13. # Todo 如何将 test_users 中的数据保存到 用户登录数据 sheet中
  14. # username password
  15. # zhangsan 123456789
  16. # lisi 123456789
  17. # wangwu 123456789
  18. sheet2.append(['username','password'])
  19. for user in test_users:
  20. # print(type(list(user.values())), user.values())
  21. sheet2.append(list(user.values())) # 因为保存Excel文件需要使用到 List 格式的数据,所以进行数据转换
  22. # # 分别在worksheet 中创建数
  23. # sheet1['A1'] = "数据1"
  24. # sheet1['B1'] = "数据2"
  25. # sheet1.append(['ceshi 1','ceshi 2'])
  26. # sheet1.append(['ceshi 11','ceshi 12'])
  27. #
  28. # sheet2.append(['用户名','密码'])
  29. # sheet2.append(['testuser1',"123456"])
  30. # 保存文件
  31. wb.save('./testdata.xlsx')

读取Excel文件

  1. # 导入加载Excel文件的方法
  2. from openpyxl import load_workbook
  3. from openpyxl.worksheet.worksheet import Worksheet
  4. # 加载文件 拿到整个workbook 对象
  5. wb = load_workbook(filename='./testdata.xlsx')
  6. # 获取所有的 worksheet
  7. print(wb.worksheets)
  8. # 读取 用户登录数据 :Worksheet 声明变量的类型,在写代码的时候可以有代码提示
  9. sheet:Worksheet = wb['用户登录数据']
  10. # 读取worksheet 中所有的数据
  11. print('行:', sheet.max_row)
  12. print('列:',sheet.max_column)
  13. # 读取每一行的内容
  14. for row in sheet.iter_rows(min_row=1,max_row=sheet.max_row,
  15. min_col=1,max_col=sheet.max_column,
  16. values_only=True):
  17. print(row)

输出数据

  1. 行: 4
  2. 列: 2
  3. ('username', 'password')
  4. ('zhangsan', '123456789')
  5. ('lisi', '123456789')
  6. ('wangwu', '123456789')

更多操作: https://realpython.com/openpyxl-excel-spreadsheets-python/

搭建app自动化框架

安装依赖

  1. pip install pytest appium-python-client

项目的基本目录

  1. apptesting
  2. - pom page object model 页面模型(组织页面,每个页面使用1个类来维护)
  3. - testcases 存档所有的测试用例
  4. - testdata 存放测试过程中的用例数据文件
  5. - screenshots 存放所有的 截图文件 (每个用例执行完成之后保存截图)
  6. - reports 存放自动化的测试报告文件
  7. - config 存放运行配置文件
  8. - conftest.py 配置文件 (定义driver,执行过程中的先后运行顺序)
  9. - main.py 程序执行的入口文件 (有很多测试用例,设置不同的用例执行场景)

项目代码地址: https://gitee.com/imzack/apptest0627/tree/master/

conftest.py

  1. from appium import webdriver
  2. import pytest
  3. import os
  4. chromedriver= os.path.join(os.path.dirname(os.path.abspath(__file__)),'drivers/chromedriver.exe')
  5. @pytest.fixture(scope='session')
  6. def driver():
  7. desired_caps = {
  8. 'platformName': 'Android', # 测试Android系统
  9. 'platformVersion': '7.1.2', # Android版本 可以在手机的设置中关于手机查看
  10. 'deviceName': '127.0.0.1:62001', # adb devices 命令查看 设置为自己的设备
  11. 'automationName': 'UiAutomator2', # 自动化引擎
  12. 'noReset': False, # 不要重置app的状态
  13. 'fullReset': False, # 不要清理app的缓存数据
  14. 'chromedriverExecutable': chromedriver, # chromedriver 对应的绝对路径
  15. 'appPackage': "org.cnodejs.android.md", # 应用的包名
  16. 'appActivity': ".ui.activity.MainActivity" # 应用的活动页名称
  17. }
  18. driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_capabilities=desired_caps)
  19. driver.implicitly_wait(5) # 全局的隐式等待时间
  20. yield driver # 将driver 传递出来
  21. driver.quit()

POM 介绍

pom 设计的核心思想 就是将不同的页面单独进行维护,在做自动化的过程中,如果前端页面进行更改,原来写自动化代码就可能不再适用,因为前端页面更改了之后,元素定位已经不再适合,自动化用例执行失败。就要重新更改代码,比较麻烦。

pom 将页面与测试用例单独封装,页面上的每个操作都单独封装起来,测试用例只需要调用封装好的方法即可。如果页面有改动。只需要改页面中封装的操作即可。

下面以登录场景为例,编写自动化

  1. 创建 登录页面 类

pom/loginpage.py

  1. from appium.webdriver.webdriver import WebDriver
  2. class LoginPage:
  3. def __init__(self,driver:WebDriver):
  4. self.driver = driver
  5. # 初始化类的时候 打开登录页面
  6. self.driver.start_activity(app_package="org.cnodejs.android.md",
  7. app_activity='.ui.activity.LoginActivity')
  8. def with_token_login(self,token):
  9. """
  10. 使用token的方式进行登录
  11. :param token:
  12. :return:
  13. """
  14. self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
  15. loginbtn = self.driver.find_element_by_android_uiautomator(
  16. 'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
  17. loginbtn.click()
  18. def with_code_login(self):
  19. pass
  20. def with_github_login(self):
  21. pass

testcases/test_user.py

  1. from pom.loginpage import LoginPage
  2. # 使用conftest.py 中定义的 driver
  3. def test_login(driver):
  4. # 打开登录页面
  5. loginpage = LoginPage(driver)
  6. # 使用token进行登录
  7. loginpage.with_token_login('tokenxxxxxxx')
  8. testcases/test_user.py

登录的方法封装完成之后,其他的自动化操作 需要使用到登录,只需要调用登录方法即可。

所有的页面对象中 都需要在类初始化的时候使用到driver,可以创建一个基类。所有的页面类都继承这个基类。
pom/basepage.py

  1. from appium.webdriver.webdriver import WebDriver
  2. class BasePage:
  3. def __init__(self,driver:WebDriver):
  4. self.driver = driver

pom/homepage.py

  1. """
  2. 首页
  3. """
  4. from pom.basepage import BasePage
  5. from pom.createtopicpage import CreateTopicPage
  6. class HomePage(BasePage):
  7. def __init__(self,driver):
  8. super(HomePage, self).__init__(driver)
  9. # 打开首页
  10. self.driver.start_activity(app_package='org.cnodejs.android.md',
  11. app_activity='.ui.activity.MainActivity')
  12. def go_create_topic(self):
  13. create_btn = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
  14. create_btn.click()
  15. return CreateTopicPage(self.driver)

pom/createtopicpage.py

  1. """
  2. 发帖页面
  3. """
  4. from pom.basepage import BasePage
  5. class CreateTopicPage(BasePage):
  6. def new_topic(self,tab,title,content):
  7. spinner = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/spn_tab")')
  8. spinner.click()
  9. tab_selcotor = f'.resourceId("android:id/text1").text("{tab}")'
  10. self.driver.find_element_by_android_uiautomator(tab_selcotor).click()
  11. title_content = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_title")')
  12. title_content.send_keys(title)
  13. content_area = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_content")')
  14. content_area.send_keys(content)
  15. send_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/action_send")')
  16. send_btn.click()
  17. @property
  18. def result_text(self):
  19. toast = self.driver.find_element_by_xpath('//android.widget.Toast')
  20. return toast.text

页面编写完成之后,编写自动化测试用例
testcases/test_topics.py

  1. from pom.homepage import HomePage
  2. from pom.loginpage import LoginPage
  3. def test_create_topic(driver):
  4. loginpage = LoginPage(driver)
  5. # 用户登录成功
  6. loginpage.with_token_login('d1563473-1f0d-4307-9774-6c2ff49c93ab')
  7. # 首页打开
  8. hp = HomePage(driver)
  9. # 进入创建话题页面
  10. create_page = hp.go_create_topic()
  11. create_page.new_topic(tab='分享',title='123',content='012')
  12. result = create_page.result_text
  13. assert result == "标题要求10字以上"

执行

  1. pytest testcases\test_topics.py -s -v

pom 代码优化

上面的代码实现了基本的页面模型。但是 使用 start_activity 方法来进行启动页面的操作在个别手机执行有问题。以登录为例,可以将登录的操作再进一步优化。

pom/basepage.py

  1. from appium.webdriver.webdriver import WebDriver
  2. import time
  3. from selenium.common.exceptions import NoSuchElementException
  4. class BasePage:
  5. def __init__(self,driver:WebDriver):
  6. self.driver = driver
  7. @property
  8. def result_text(self):
  9. """
  10. 获取Toast的文本值
  11. :return:
  12. """
  13. try:
  14. toast = self.driver.find_element_by_xpath('//android.widget.Toast')
  15. return toast.text
  16. except NoSuchElementException:
  17. return "找不到这个元素,请检查自己的自动化代码"

pom/createtopicpage.py

  1. """
  2. 发帖页面
  3. """
  4. from pom.basepage import BasePage
  5. class CreateTopicPage(BasePage):
  6. def new_topic(self,tab,title,content):
  7. spinner = self.driver.find_element_by_android_uiautomator('.resourceId("org.cnodejs.android.md:id/spn_tab")')
  8. spinner.click()
  9. tab_selcotor = f'.resourceId("android:id/text1").text("{tab}")'
  10. self.driver.find_element_by_android_uiautomator(tab_selcotor).click()
  11. title_content = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_title")')
  12. title_content.send_keys(title)
  13. content_area = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/edt_content")')
  14. content_area.send_keys(content)
  15. send_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/action_send")')
  16. send_btn.click()

pom/loginpage.py

  1. from appium.webdriver.webdriver import WebDriver
  2. import time
  3. from pom.basepage import BasePage
  4. class LoginPage(BasePage):
  5. def __init__(self,driver:WebDriver):
  6. super(LoginPage, self).__init__(driver)
  7. # 保证页面是在登录页面上
  8. # 判断当前driver的状态
  9. current_activity = self.driver.current_activity
  10. if ".ui.activity.LoginActivity" in current_activity:
  11. pass
  12. else:
  13. # 打开登录页面
  14. self.__go_login_page()
  15. # 初始化类的时候 打开这个页面
  16. # self.driver.start_activity(app_package="org.cnodejs.android.md",
  17. # app_activity='.ui.activity.LoginActivity')
  18. def with_token_login(self,token):
  19. """
  20. 使用token的方式进行登录
  21. :param token:
  22. :return:
  23. """
  24. self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
  25. loginbtn = self.driver.find_element_by_android_uiautomator(
  26. 'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
  27. loginbtn.click()
  28. def __go_login_page(self):
  29. """
  30. 导航到loginpage
  31. :return:
  32. """
  33. # 清空app的状态 如果已经登录,去掉登录状态
  34. self.driver.reset()
  35. # 打开首页
  36. self.driver.start_activity(app_package='org.cnodejs.android.md',app_activity='.ui.activity.LaunchActivity')
  37. toggle_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/toolbar")'
  38. '.childSelector(new UiSelector().className("android.widget.ImageButton"))')
  39. toggle_btn.click()
  40. time.sleep(1)
  41. # 点击头像
  42. avatar = self.driver.find_element_by_android_uiautomator('text("点击头像登录")'
  43. '.resourceId("org.cnodejs.android.md:id/tv_login_name")')
  44. avatar.click()
  45. def with_code_login(self):
  46. pass
  47. def with_github_login(self):
  48. pass

testcases/test_user.py

  1. from pom.loginpage import LoginPage
  2. # 使用conftest.py 中定义的 driver
  3. def test_login(driver):
  4. # 打开登录页面
  5. loginpage = LoginPage(driver)
  6. # 使用token进行登录
  7. loginpage.with_token_login('tokenxxxxxxx')
  8. # 登录成功, 验证toast的文本值为登录成功
  9. result = loginpage.result_text
  10. assert result == "登录成功"

appium会启动app的加载页
conftest.py

  1. from appium import webdriver
  2. import pytest
  3. import os
  4. chromedriver= os.path.join(os.path.dirname(os.path.abspath(__file__)),'drivers/chromedriver.exe')
  5. @pytest.fixture(scope='session')
  6. def driver():
  7. desired_caps = {
  8. 'platformName': 'Android', # 测试Android系统
  9. 'platformVersion': '7.1.2', # Android版本 可以在手机的设置中关于手机查看
  10. 'deviceName': '127.0.0.1:62001', # adb devices 命令查看 设置为自己的设备
  11. 'automationName': 'UiAutomator2', # 自动化引擎
  12. 'noReset': False, # 不要重置app的状态
  13. 'fullReset': False, # 不要清理app的缓存数据
  14. 'chromedriverExecutable': chromedriver, # chromedriver 对应的绝对路径
  15. 'appPackage': "org.cnodejs.android.md", # 应用的包名
  16. 'appActivity': ".ui.activity.LaunchActivity" # 应用的活动页名称
  17. }
  18. driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_capabilities=desired_caps)
  19. driver.implicitly_wait(5) # 全局的隐式等待时间
  20. yield driver # 将driver 传递出来
  21. driver.quit()

修改homepage.py
pom/homepage.py

  1. """
  2. 首页
  3. """
  4. from pom.basepage import BasePage
  5. from pom.createtopicpage import CreateTopicPage
  6. class HomePage(BasePage):
  7. def __init__(self,driver):
  8. super(HomePage, self).__init__(driver)
  9. if '.ui.activity.MainActivity' in self.driver.current_activity:
  10. pass
  11. else:
  12. self.__go_home_page()
  13. # # 打开首页
  14. # self.driver.start_activity(app_package='org.cnodejs.android.md',
  15. # app_activity='.ui.activity.MainActivity')
  16. def __go_home_page(self):
  17. self.driver.start_activity(app_activity='.ui.activity.LaunchActivity')
  18. def go_create_topic(self):
  19. # 判断是否已经到达创建话题页面
  20. while not '.ui.activity.CreateTopicActivity' in self.driver.current_activity:
  21. # 再重新点击一下
  22. create_btn = self.driver.find_element_by_android_uiautomator(
  23. '.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
  24. create_btn.click()
  25. return CreateTopicPage(self.driver)

至此,pom的方式大家可以按照上述来进行编写即可。

Excel数据驱动

登录场景,发布话题场场景 都需要做数据驱动。

在项目中封装读取Excel文件的代码

添加Excel文件
testdata/data.xlsx

data.xlsx

Excel文件处理
utils/file_handler.py

  1. import os
  2. from openpyxl import load_workbook
  3. from openpyxl.worksheet.worksheet import Worksheet
  4. class FileHandler:
  5. def __init__(self):
  6. self.__excel_path = os.path.join(os.path.dirname( os.path.abspath(__file__)),'../testdata/data.xlsx')
  7. def get_data_by_sheet(self,sheetName):
  8. wb = load_workbook(self.__excel_path)
  9. if sheetName not in wb.sheetnames:
  10. raise Exception(f"请检查sheet的名字,{sheetName}不在 {wb.sheetnames}中")
  11. sheet:Worksheet = wb[sheetName]
  12. sheet_data = []
  13. for row in sheet.iter_rows(min_row=2,max_row=sheet.max_row,
  14. min_col=1,max_col=sheet.max_column,values_only=True):
  15. sheet_data.append(row)
  16. return sheet_data

pom/homepage.py

  1. """
  2. 首页
  3. """
  4. from pom.basepage import BasePage
  5. from pom.createtopicpage import CreateTopicPage
  6. class HomePage(BasePage):
  7. def __init__(self,driver):
  8. super(HomePage, self).__init__(driver)
  9. if '.ui.activity.MainActivity' in self.driver.current_activity:
  10. pass
  11. else:
  12. self.__go_home_page()
  13. # # 打开首页
  14. # self.driver.start_activity(app_package='org.cnodejs.android.md',
  15. # app_activity='.ui.activity.MainActivity')
  16. def __go_home_page(self):
  17. self.driver.start_activity(app_package='org.cnodejs.android.md', app_activity='.ui.activity.LaunchActivity')
  18. def go_create_topic(self):
  19. # 判断是否已经到达创建话题页面
  20. while not '.ui.activity.CreateTopicActivity' in self.driver.current_activity:
  21. # 再重新点击一下
  22. create_btn = self.driver.find_element_by_android_uiautomator(
  23. '.resourceId("org.cnodejs.android.md:id/fab_create_topic")')
  24. create_btn.click()
  25. return CreateTopicPage(self.driver)

pom/loginpage.py

  1. from appium.webdriver.webdriver import WebDriver
  2. import time
  3. from pom.basepage import BasePage
  4. class LoginPage(BasePage):
  5. def __init__(self,driver:WebDriver):
  6. super(LoginPage, self).__init__(driver)
  7. # 保证页面是在登录页面上
  8. # 判断当前driver的状态
  9. current_activity = self.driver.current_activity
  10. if ".ui.activity.LoginActivity" in current_activity:
  11. pass
  12. else:
  13. # 打开登录页面
  14. self.__go_login_page()
  15. # 初始化类的时候 打开这个页面
  16. # self.driver.start_activity(app_package="org.cnodejs.android.md",
  17. # app_activity='.ui.activity.LoginActivity')
  18. def with_token_login(self,token):
  19. """
  20. 使用token的方式进行登录
  21. :param token:
  22. :return:
  23. """
  24. self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token').send_keys(token)
  25. loginbtn = self.driver.find_element_by_android_uiautomator(
  26. 'text("登录").resourceId("org.cnodejs.android.md:id/btn_login")')
  27. loginbtn.click()
  28. def __go_login_page(self):
  29. """
  30. 导航到loginpage
  31. :return:
  32. """
  33. # 清空app的状态 如果已经登录,去掉登录状态
  34. self.driver.reset()
  35. # 打开首页
  36. self.driver.start_activity(app_package='org.cnodejs.android.md',app_activity='.ui.activity.LaunchActivity')
  37. toggle_btn = self.driver.find_element_by_android_uiautomator('resourceId("org.cnodejs.android.md:id/toolbar")'
  38. '.childSelector(new UiSelector().className("android.widget.ImageButton"))')
  39. toggle_btn.click()
  40. time.sleep(1)
  41. # 点击头像
  42. avatar = self.driver.find_element_by_android_uiautomator('text("点击头像登录")'
  43. '.resourceId("org.cnodejs.android.md:id/tv_login_name")')
  44. avatar.click()
  45. @property
  46. def with_token_failed_text(self):
  47. # 1. 截图
  48. ele = self.driver.find_element_by_id('org.cnodejs.android.md:id/edt_access_token')
  49. png = ele.screenshot_as_base64
  50. # 2. TODO 调用 ocr 图片识别 将图片中文字识别出来
  51. return ""
  52. def with_code_login(self):
  53. pass
  54. def with_github_login(self):
  55. pass

testcases/test_ddt/test_ddt_login.py

  1. import pytest
  2. from pom.loginpage import LoginPage
  3. from utils.file_handler import FileHandler
  4. fl = FileHandler()
  5. # 从Excel文件中获取数据
  6. data = fl.get_data_by_sheet('用户登录')
  7. class TestLogin:
  8. @pytest.mark.parametrize('token,status,expect_val',data)
  9. def test_login(self,driver,token,status,expect_val):
  10. # 打开登录页面
  11. loginpage = LoginPage(driver)
  12. # 使用token进行登录
  13. loginpage.with_token_login(token)
  14. if status == '成功':
  15. # 登录成功, 验证toast的文本值为登录成功
  16. result = loginpage.result_text
  17. assert result == expect_val
  18. if status == "失败":
  19. result = loginpage.with_token_failed_text
  20. assert result == expect_val

测试报告

pytest-html https://pypi.org/project/pytest-html/
pytest-allure https://pypi.org/project/allure-pytest/

pytest html

  1. pip install pytest-html

添加执行文件
main.py

  1. import pytest
  2. import os,time
  3. if __name__ == '__main__':
  4. report_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),'reports')
  5. if not os.path.exists(report_dir):
  6. os.mkdir(report_dir)
  7. report = time.strftime('%Y_%m_%d_%H_%M_%S')
  8. reportfile = os.path.join(report_dir,report+'.html')
  9. pytest.main(['testcases','-s','-v',f'--html={reportfile}'])

直接执行 main.py 文件,得出测试报告;

image.png

附录

在pycharm中使用 git 提交代码
https://www.bilibili.com/video/BV15f4y1s7Rv?p=2

整个项目的代码
https://gitee.com/imzack/apptest0627