UiAutoTest
一、概要
數據驅動的Ui自動化框架
二、環境要求
- 框架基於Python3 + unittest + appium
- 運行電腦需配置adb、aapt的環境變量,build_tools版本建議選擇28及以上
- 配置appium環境,並確保appium版本1.9及以上
- 目前只支持安卓手機,建議使用安卓7.0及以上設備
- 運行時候,電腦只能同時連接一台測試機
三、框架結構和原理
3.1 框架原理
框架結構設計分為四層,自下而上分別為:基礎工具類Base
層、頁面操作PageObject
層、測試用例集TestCase
層和主程序runAll.py
框架的大致工作流程為:主程序runAll.py
使用unittest
的discover
方法從TestCase
批量添加測試用例並執行,而每一個測試用例都調用PageObject/Page.py
模塊下的operate/check
方法來做執行/斷言
操作,operate/check
方法首先調用Base/BaseGetParams.py
模塊下的get_operate_params/get_check_params
方法組裝來自Excel
的執行/斷言
數據,然后再調用Base/BaseOperate.py
模塊下的各種操作方法完成對應的元素操作動作,其結構如下圖:
3.2 各模塊說明
- App - 存放待測apk
- Base - 存放基礎工具類
- DrivingData - 存放測試驅動數據(excel格式)
- Log - 存放日志
- PageObject - 封裝頁面的操作邏輯和校驗邏輯
- Result - 存放測試結果
- TestCase - 存放測試用例
- config.ini - 配置文件
- runAll.py - 主腳本
四、框架使用說明
使用該框架的流程主要有以下四步:
- 第一步,按照
Excel
數據格式填寫驅動數據 - 第二步,在
TestCase
里寫測試用例腳本 - 第三步,按照需求修改配置文件
config.ini
- 第四步,運行主程序
runAll.py
以下,為該框架詳細使用說明
-
Excel驅動數據 - 各字段詳細說明(*表示重要字段)
- *step:執行步驟,可選operate和check,operate表示操作,check表示斷言
- step_desc:執行步驟描述,打印日志使用
- page_object:操作步驟所處的頁面描述,打印日志使用
- *location_type:常用定位方式, 可選方式為:id,xpath,name,class_name,css_selector,ids,xpaths,names,class_names,css_selectors,結尾為
s
的表示定位一組元素 - index:索引,如果location_type用定位一組元素,用index索引確定具體的元素,索引的下表從0開始,即要取組元素中的第一個元素,index為0
- *location_value:定位值,注意定位值里面均為英文字符,如有中文會導致無法正確定位
- *operate_method:常用操作方法,可選方法為:click,input,wait,imp_wait,close_allow_box,swipe_by_ratio,text,screenshot,quit
- *operate_value:操作值,如有多個,中間以空格隔開
- execution:運行標志位,
Yes
為需要執行,No
不執行
-
Excel驅動數據 - operate_method詳細說明
- click - 點擊事件,location_type、location_value為必填字段,operate_value為空
- input - 清除輸入框文本並輸入事件,location_type、location_value、operate_value為必填字段,其中operate_value填寫輸入內容
- wait - 強制等待事件,location_type、location_value為空,operate_value(非必填項)填數字,如不填默認等待時間為1s,否則按照填寫時間執行
- imp_wait - 隱式等待,location_type、location_value為空,operate_value(非必填項)填數字,如不填默認等待時間為5s,否則按照填寫時間執行
- close_allow_box - 關閉權限彈框事件,location_type、location_value為空,operate_value(非必填項)填寫格式為:權限button內容 關閉次數,如不填默認匹配
允許
字符,關閉次數為1次 - swipe_by_ratio - 滑動事件,location_type、location_value為空,operate_value(非必填項)填寫格式:開始坐標 方向 滑動比例 持續時間(毫秒),如不填默認
start_x=500, start_y=800, direction='up', ratio=0.1, duration=200
執行 - text - 獲取元素文本,location_type、location_value為空,operate_value(非必填項),通常用於check方法下,獲得的元素文本與operate_value內容作對比
- screenshot - 截圖事件,location_type、location_value為空,operate_value(非必填項)填寫內容為截圖文件名,如不填默認為screenshot+當前時間戳
- quit - 退出時間,一般用於運行結束,關閉app
- is_login - 判斷是否登錄,如果是則退出,回到登錄頁,如果不是則直接進入登錄頁,location_type、location_value為空,operate_value(非必填項),填寫內容為:登錄標志字段 設置按鈕的定位方式 設置按鈕的定位值,如:
登錄 id com.aspirecn.xiaoxuntongParent.ln:id/me_setting
is_login方法源碼 - BaseOperate.py
def is_login(self, login_text='登錄', set_loc_type='id', set_loc='com.aspirecn.xiaoxuntongTeacher.ln:id/me_setting'): """ 判斷是否為登錄狀態,是退出,否進入登錄頁 :param login_text: 判斷是否登錄的標志字段 :param set_loc_type: 設置元素定位方式 :param set_loc: 設置元素定位 :return: """ wo_loc = "//*[@text='我']" login_text_loc = "//*[contains(@text, '%s')]" % login_text wo_el = self.find_element('xpath', wo_loc) self.to_click(wo_el[1]) if wo_el[0] else logger.info("切換'我'定位失敗,請檢查xpath是否正確:%s" % wo_loc) if self.find_element('xpath', login_text_loc)[0]: logger.info('the app is logout status') self.to_click(self.find_element('xpath', login_text_loc)[1]) else: set_el = self.find_element(set_loc_type, set_loc) self.to_click(set_el[1]) if set_el[0] else logger.info("'設置'定位失敗,請檢查%s是否正確:%s" % (set_loc_type, wo_loc)) logout_el = self.find_element('xpath', "//*[contains(@text, '退出')]") self.to_click(logout_el[1]) if logout_el[0] else logger.info("'退出'定位失敗,請檢查xpath是否正確:%s" % "//*[contains(@text, '退出')]")
swipe_by_ratio方法源碼 - BaseOperate.py
def swipe_by_ratio(self, start_x=500, start_y=800, direction='up', ratio=0.1, duration=200): """ The function of swipe by ratio(按比例滑動屏幕) :param start_x: start abscissa :param start_y: start ordinate :param direction: The direction of swipe, support 'up', 'down', 'left', 'right' :param ratio: The screen ration's distance of swiping :param duration: continue time, unit ms :return: """ self.force_wait(3) direction_tuple = ('up', 'down', 'left', 'right') if direction not in direction_tuple: logger.info('This swiping direction is not support') width, height = self.get_screen_size() def swipe_up(): """ swipe to up :return: """ end_y = start_y - height * ratio if end_y < 0: logger.info('the distance of swiping too much') else: self.driver.swipe(start_x, start_y, start_x, end_y, duration) logger.info('from (%s, %s) swiping up to (%s, %s), during %sms' % (start_x, start_y, start_x, end_y, duration)) def swipe_down(): """ swipe to down :return: """ end_y = start_y + height * ratio if end_y > height: logger.info('the distance of swiping too much') else: self.driver.swipe(start_x, start_y, start_x, end_y, duration) logger.info('from (%s, %s) swiping down to (%s, %s), during %sms' % (start_x, start_y, start_x, end_y, duration)) def swipe_left(): """ swipe to left :return: """ end_x = start_x - width * ratio if end_x < 0: logger.info('the distance of swiping too much') else: self.driver.swipe(start_x, start_y, end_x, start_y, duration) logger.info('from (%s, %s) swiping left to (%s, %s), during %sms' % (start_x, start_y, end_x, start_y, duration)) def swipe_right(): """ swipe to right :return: """ end_x = start_x + width * ratio if end_x > width: logger.info('the distance of swiping too much') else: self.driver.swipe(start_x, start_y, end_x, start_y, duration) logger.info('from (%s, %s) swiping right to (%s, %s), during %sms' % (start_x, start_y, end_x, start_y, duration)) swipe_dict = { 'up': swipe_up, 'down': swipe_down, 'left': swipe_left, 'right': swipe_right } # 【錯誤】,以下寫法邏輯有問題,即在初始化字典時候就已經執行了所有滑動方法,最后還要返回的是swipe_dict[direction]函數 # swipe_dict = {'up': swipe_up(),'down': swipe_down(),...} # return swipe_dict[direction] # 【正確】,字典中只初始化方向對應的函數,return返回具體的滑動函數調用 return swipe_dict[direction]()
-
測試用例集命名格式為:test_n_xxx.py,其中n為阿拉伯數字,可用數字大小控制用例集執行的先后順序,xxx為腳本名,如:
test_2_notice.py
-
測試用例編寫格式
- 測試用例命名格式與測試用例集命名格式類似,可用數字大小控制用例集執行的先后順序
- 每一個case需要傳入不同的sheet,用於取數據
- 如果該條case無check,則不用寫
self.assertEqual(page.check()[0], True, msg=page.check()[1])
- 另外,在最后的
setUpClass
和tearDownClass
方法下的super
類中寫當前測試用例集的類名,如:super(SendNotice, cls).setUpClass()
,SendNotice
為該用例集的類名 - 其余內容復制粘貼即可,詳情如以下代碼片段所示
from PageObject.Pages import PageObjects from Base.BaseRunner import ParametrizedTestCase class SendNotice(ParametrizedTestCase): """ 發通知測試 """ def test_1_send_text(self): """發文本通知""" app = { 'driver': self.driver, 'data_path': self.data_path, 'sheet': 'text_notice' } page = PageObjects(app) page.operate() self.assertEqual(page.check()[0], True, msg=page.check()[1]) def test_2_send_rich_text(self): """發圖文音頻通知""" app = { 'driver': self.driver, 'data_path': self.data_path, 'sheet': 'rich_text_notice' } page = PageObjects(app) page.operate() self.assertEqual(page.check()[0], True, msg=page.check()[1]) @classmethod def setUpClass(cls): super(SendNotice, cls).setUpClass() @classmethod def tearDownClass(cls): super(SendNotice, cls).tearDownClass()
-
配置文件說明
# --- PATH CONFIG --- # # 注意:以下均以使用該路徑的py文件為當前路徑的相對路徑表達式 [LOG_PATH] path = ../Log [RESULT_PATH] path = ./Result/ # teacher [T_DATA_PATH] path = ./DrivingData/teacher.xlsx [T_APK_PATH] path = ./App/T_XXT_LN_35.apk [T_CASE_PATH] path = ./TestCase/teacher # parent [P_DATA_PATH] path = ./DrivingData/parent.xlsx [P_APK_PATH] path = ./App/P_XXT_LN_37.apk [P_CASE_PATH] path = ./TestCase/parent # --- OTHER FUNC CONFIG --- # [WAIT] # 顯示等待時間 time = 6 [NORESET] # 重置應用(清除session信息),True為不需要重置,False為需要重置 noReset = True [PORT] # appium服務的端口號,默認為4723,啟動時請確認appium的端口號與之一致 port = 4723 [APP_TYPE] # 啟動的客戶端類型,若啟動啟動兩個客戶端`teacher`和`parent`中間用英文','隔開即可 role_lst = teacher,parent # role_lst = teacher # role_lst = parent [OUTPUT] # 運行結果輸出的方式,to_html為生成HTML測試報告,to_console為結果輸出在控制器 # method = to_html method = to_console
參考
OK
~
~
~
不積跬步,無以至千里