基於Python3 + appium的Ui自動化測試框架


UiAutoTest

一、概要

數據驅動的Ui自動化框架

二、環境要求

  1. 框架基於Python3 + unittest + appium
  2. 運行電腦需配置adb、aapt的環境變量,build_tools版本建議選擇28及以上
  3. 配置appium環境,並確保appium版本1.9及以上
  4. 目前只支持安卓手機,建議使用安卓7.0及以上設備
  5. 運行時候,電腦只能同時連接一台測試機

三、框架結構和原理

3.1 框架原理

框架結構設計分為四層,自下而上分別為:基礎工具類Base層、頁面操作PageObject層、測試用例集TestCase層和主程序runAll.py
框架的大致工作流程為:主程序runAll.py使用unittestdiscover方法從TestCase批量添加測試用例並執行,而每一個測試用例都調用PageObject/Page.py模塊下的operate/check方法來做執行/斷言操作,operate/check方法首先調用Base/BaseGetParams.py模塊下的get_operate_params/get_check_params方法組裝來自Excel執行/斷言數據,然后再調用Base/BaseOperate.py模塊下的各種操作方法完成對應的元素操作動作,其結構如下圖:
框架結構

3.2 各模塊說明
  1. App - 存放待測apk
  2. Base - 存放基礎工具類
  3. DrivingData - 存放測試驅動數據(excel格式)
  4. Log - 存放日志
  5. PageObject - 封裝頁面的操作邏輯和校驗邏輯
  6. Result - 存放測試結果
  7. TestCase - 存放測試用例
  8. config.ini - 配置文件
  9. runAll.py - 主腳本

四、框架使用說明

使用該框架的流程主要有以下四步:

  • 第一步,按照Excel數據格式填寫驅動數據
  • 第二步,在TestCase里寫測試用例腳本
  • 第三步,按照需求修改配置文件config.ini
  • 第四步,運行主程序runAll.py

以下,為該框架詳細使用說明

  1. Excel驅動數據 - 各字段詳細說明(*表示重要字段)
    Excel驅動數據格式

    1. *step:執行步驟,可選operate和check,operate表示操作,check表示斷言
    2. step_desc:執行步驟描述,打印日志使用
    3. page_object:操作步驟所處的頁面描述,打印日志使用
    4. *location_type:常用定位方式, 可選方式為:id,xpath,name,class_name,css_selector,ids,xpaths,names,class_names,css_selectors,結尾為s的表示定位一組元素
    5. index:索引,如果location_type用定位一組元素,用index索引確定具體的元素,索引的下表從0開始,即要取組元素中的第一個元素,index為0
    6. *location_value:定位值,注意定位值里面均為英文字符,如有中文會導致無法正確定位
    7. *operate_method:常用操作方法,可選方法為:click,input,wait,imp_wait,close_allow_box,swipe_by_ratio,text,screenshot,quit
    8. *operate_value:操作值,如有多個,中間以空格隔開
    9. execution:運行標志位,Yes為需要執行,No不執行
  2. Excel驅動數據 - operate_method詳細說明
    operate_method

    1. click - 點擊事件,location_type、location_value為必填字段,operate_value為空
    2. input - 清除輸入框文本並輸入事件,location_type、location_value、operate_value為必填字段,其中operate_value填寫輸入內容
    3. wait - 強制等待事件,location_type、location_value為空,operate_value(非必填項)填數字,如不填默認等待時間為1s,否則按照填寫時間執行
    4. imp_wait - 隱式等待,location_type、location_value為空,operate_value(非必填項)填數字,如不填默認等待時間為5s,否則按照填寫時間執行
    5. close_allow_box - 關閉權限彈框事件,location_type、location_value為空,operate_value(非必填項)填寫格式為:權限button內容 關閉次數,如不填默認匹配允許字符,關閉次數為1次
    6. swipe_by_ratio - 滑動事件,location_type、location_value為空,operate_value(非必填項)填寫格式:開始坐標 方向 滑動比例 持續時間(毫秒),如不填默認start_x=500, start_y=800, direction='up', ratio=0.1, duration=200執行
    7. text - 獲取元素文本,location_type、location_value為空,operate_value(非必填項),通常用於check方法下,獲得的元素文本與operate_value內容作對比
    8. screenshot - 截圖事件,location_type、location_value為空,operate_value(非必填項)填寫內容為截圖文件名,如不填默認為screenshot+當前時間戳
    9. quit - 退出時間,一般用於運行結束,關閉app
    10. 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]()
    
    
  3. 測試用例集命名格式為:test_n_xxx.py,其中n為阿拉伯數字,可用數字大小控制用例集執行的先后順序,xxx為腳本名,如:test_2_notice.py

  4. 測試用例編寫格式

    1. 測試用例命名格式與測試用例集命名格式類似,可用數字大小控制用例集執行的先后順序
    2. 每一個case需要傳入不同的sheet,用於取數據
    3. 如果該條case無check,則不用寫self.assertEqual(page.check()[0], True, msg=page.check()[1])
    4. 另外,在最后的setUpClasstearDownClass方法下的super類中寫當前測試用例集的類名,如:super(SendNotice, cls).setUpClass()SendNotice為該用例集的類名
    5. 其余內容復制粘貼即可,詳情如以下代碼片段所示
    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()
    
  5. 配置文件說明

    # --- 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
    

參考

  1. https://github.com/Louis-me/appium
  2. https://github.com/Lemonzhulixin/python-appium

OK

~
~
~

不積跬步,無以至千里


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM