Appium自動化封裝教案


導入如下包

 

 

 

 

框架背景

前面我們已經學習了Appium各種元素定位,手勢操作、數據配置、Pageobject設計模式等等。但是前面的功能都是比較零散的,沒有整體融合起來,實際項目實踐過程中我們需要綜合運用,那么本章節我們將結合之前所學的內容,從0到1搭建一個完整的自動化測試框架。

 

框架功能

  • 業務功能的封裝
  • 測試用例封裝
  • 測試包管理
  • 截圖處理
  • 斷言處理
  • 日志獲取
  • 測試報告生成
  • 數據驅動
  • 數據配置

 

測試案例

測試環境

  • Win10 64Bit
  • Appium 1.7.2
  • 考研幫App Android版3.1.0
  • 夜神模擬器 Android 5.1.1

覆蓋用例

1.登錄場景

用戶名

密碼

自學網2018

zxw2018

自學網2017

zxw2017

666

222

2.注冊場景

注冊一個新的賬號(賬戶和密碼可以隨機生成),完善院校和專業信息 (如:院校:上海-同濟大學 專業:經濟學類-統計學-經濟統計學)

框架設計圖

 

 

代碼實現

driver配置封裝

kyb_caps.yaml 配置表

platformName: Android
#模擬器
platformVersion: 5.1.1
deviceName: 127.0.0.1:62025

#mx4真機
#platformVersion: 5.1
#udid: 750BBKL22GDN
#deviceName: MX4

appname: kaoyan3.1.0.apk
noReset: False
unicodeKeyboard: True
resetKeyboard: True

appPackage: com.tal.kaoyan
appActivity: com.tal.kaoyan.ui.activity.SplashActivity
ip: 127.0.0.1
port: 4723

desired_caps.py

import yaml
import logging.config
from appium import webdriver
import os

CON_LOG = '../config/log.conf'
logging.config.fileConfig(CON_LOG)
logging = logging.getLogger()

def appium_desired():

    with open('../config/kyb_caps.yaml','r',encoding='utf-8') as file:
        data = yaml.load(file)

    desired_caps={}
    desired_caps['platformName']=data['platformName']

    desired_caps['platformVersion']=data['platformVersion']
    desired_caps['deviceName']=data['deviceName']

    base_dir = os.path.dirname(os.path.dirname(__file__))
    app_path = os.path.join(base_dir, 'app', data['appname'])
    desired_caps['app'] = app_path

    desired_caps['noReset']=data['noReset']

    desired_caps['unicodeKeyboard']=data['unicodeKeyboard']
    desired_caps['resetKeyboard']=data['resetKeyboard']

    desired_caps['appPackage']=data['appPackage']
    desired_caps['appActivity']=data['appActivity']

    logging.info('start run app...')
    driver = webdriver.Remote('http://'+str(data['ip'])+':'+str(data['port'])+'/wd/hub', desired_caps)

    driver.implicitly_wait(5)
    return driver


if __name__ == '__main__':
    appium_desired()

    # with open('../config/kyb_caps.yaml','r',encoding='utf-8') as file:
    #    data = yaml.load(file)

    #base_dir = os.path.dirname(os.path.dirname(__file__))
    #app_path = os.path.join(base_dir, 'app', data['appname'])
    #print(app_path)

 

相對路徑符號含義
  1. “.”表示當前目錄
  2. “..” 表示當前目錄的上一級目錄。
  3. “./”表示當前目錄下的某個文件或文件夾,視后面跟着的名字而定
  4. “../”表示當前目錄上一級目錄的文件或文件夾,視后面跟着的名字而定。

基類封裝

baseView.py

class BaseView(object):
    def __init__(self,driver):
        self.driver=driver

    def find_element(self,*loc):
        return self.driver.find_element(*loc)

    def find_elements(self,*loc):
        return self.driver.find_elements(*loc)

    def get_window_size(self):
        return self.driver.get_window_size()

    def swipe(self,start_x, start_y, end_x, end_y, duration):
        return self.driver.swipe(start_x, start_y, end_x, end_y, duration)

 

common公共模塊封裝

公共方法封裝 : common_fun.py

from  baseView.baseView import BaseView
from common.desired_caps import appium_desired
from selenium.common.exceptions import NoSuchElementException
import logging.config
from selenium.webdriver.common.by import By
import os
import time
import csv


class Common(BaseView):

    #取消升級和跳過引導按鈕
    cancel_upgradeBtn=(By.ID,'android:id/button2')
    skipBtn=(By.ID,'com.tal.kaoyan:id/tv_skip')

    # 登錄后浮窗廣告取消按鈕
    wemedia_cacel=(By.ID, 'com.tal.kaoyan:id/view_wemedia_cacel')

    def check_updateBtn(self):
        logging.info("============check_updateBtn===============")

        try:
            element = self.driver.find_element(*self.cancel_upgradeBtn)
        except NoSuchElementException:
            logging.info('update element is not found!')
        else:
            logging.info('click cancelBtn')
            element.click()

    def check_skipBtn(self):
        logging.info("==========check_skipBtn===========")
        try:
            element = self.driver.find_element(*self.skipBtn)
        except NoSuchElementException:
            logging.info('skipBtn element is not found!')
        else:
            logging.info('click skipBtn')
            element.click()

    def get_screenSize(self):
        '''
        獲取屏幕尺寸
        :return: 
        '''
        x = self.get_window_size()['width']
        y = self.get_window_size()['height']
        return (x, y)


    def swipeLeft(self):
        logging.info('swipeLeft')
        l = self.get_screenSize()
        y1 = int(l[1] * 0.5)
        x1 = int(l[0] * 0.95)
        x2 = int(l[0] * 0.25)
        self.swipe(x1, y1, x2, y1, 1000)



    def getTime(self):
        self.now = time.strftime("%Y-%m-%d %H_%M_%S")
        return self.now

    def getScreenShot(self, module):
        time = self.getTime()
        image_file= os.path.dirname(os.path.dirname(__file__)) + '/screenshots/%s_%s.png' % (module, time)

        logging.info('get %s screenshot' % module)
        self.driver.get_screenshot_as_file(image_file)

    def check_market_ad(self):
        '''檢測登錄或者注冊之后的界面浮窗廣告'''
        logging.info('=======check_market_ad=============')
        try:
            element=self.driver.find_element(*self.wemedia_cacel)
        except NoSuchElementException:
            pass
        else:
            logging.info('close market ad')
            element.click()
    
    def get_csv_data(self,csv_file,line):
        '''
        獲取csv文件指定行的數據
        :param csv_file: csv文件路徑
        :param line: 數據行數
        :return: 
        '''
        with open(csv_file, 'r', encoding='utf-8-sig') as file:
            reader=csv.reader(file)
            for index, row in enumerate(reader,1):
                if index == line:
                    return row
    

if __name__ == '__main__':
    driver=appium_desired()
    # c=Common(driver)
    # c.check_updateBtn()
    # # c.check_skipBtn()
    # c.swipeLef()
    # c.swipeLef()
    # c.getScreenShot("startApp")

業務模塊封裝

1.登錄頁面業務邏輯模塊

loginView.py

import logging
from common.desired_caps import appium_desired
from common.common_fun import Common,By
from selenium.common.exceptions import NoSuchElementException

class LoginView(Common):
    #登錄界面元素
    username_type=(By.ID,'com.tal.kaoyan:id/login_email_edittext')
    password_type=(By.ID,'com.tal.kaoyan:id/login_password_edittext')
    loginBtn=(By.ID,'com.tal.kaoyan:id/login_login_btn')

    #個人中心元素
    username=(By.ID,'com.tal.kaoyan:id/activity_usercenter_username')
    button_myself=(By.ID,'com.tal.kaoyan:id/mainactivity_button_mysefl')

    # 個人中心下線警告提醒確定按鈕
    commitBtn = (By.ID, 'com.tal.kaoyan:id/tip_commit')

    #退出操作相關元素
    settingBtn = (By.ID, 'com.tal.kaoyan:id/myapptitle_RightButtonWraper')
    logoutBtn=(By.ID,'com.tal.kaoyan:id/setting_logout_text')
    tip_commit=(By.ID,'com.tal.kaoyan:id/tip_commit')

    def login_action(self,username,password):
        self.check_updateBtn()
        self.check_skipBtn()

        logging.info('============login_action==============')
        logging.info('username is:%s' % username)
        self.driver.find_element(*self.username_type).send_keys(username)

        logging.info('password is:%s' % password)
        self.driver.find_element(*self.password_type).send_keys(password)

        logging.info('click loginBtn')
        self.driver.find_element(*self.loginBtn).click()
        logging.info('login finished!')

    def check_account_alert(self):
        '''檢測賬戶登錄后是否有賬戶下線提示'''
        logging.info('====check_account_alert======')
        try:
            element = self.driver.find_element(*self.commitBtn)
        except NoSuchElementException:
            pass
        else:
            logging.info('click commitBtn')
            element.click()


    def check_loginStatus(self):
        logging.info('==========check_loginStatus===========')
        self.check_market_ad()
        self.check_account_alert()
        
        try:
            self.driver.find_element(*self.button_myself).click()
            self.driver.find_element(*self.username)
        except NoSuchElementException:
            logging.error('login Fail!')
            self.getScreenShot('login Fail')
            return False
        else:
            logging.info('login success!')
            l.logout_action()
            return True

    def logout_action(self):
        logging.info('=========logout_action==========')
        self.driver.find_element(*self.settingBtn).click()
        self.driver.find_element(*self.logoutBtn).click()
        self.driver.find_element(*self.tip_commit).click()

if __name__ == '__main__':
    driver=appium_desired()
    l=LoginView(driver)
    l.login_action('自學網2018','zxw2018')
    l.check_loginStatus()

注冊頁面業務邏輯封裝

registerView.py

import logging
from common.desired_caps import appium_desired
from common.common_fun import Common,By,NoSuchElementException
import random

class RegisterView(Common):
    #登錄界面注冊按鈕
    register_text=(By.ID,'com.tal.kaoyan:id/login_register_text')

    #頭像設置相關元素
    userheader=(By.ID,'com.tal.kaoyan:id/activity_register_userheader')
    item_image=(By.ID,'com.tal.kaoyan:id/item_image')
    saveBtn=(By.ID,'com.tal.kaoyan:id/save')

    # 注冊-個人信息界面元素
    register_username=(By.ID,'com.tal.kaoyan:id/activity_register_username_edittext')
    register_password=(By.ID,'com.tal.kaoyan:id/activity_register_password_edittext')
    register_email=(By.ID,'com.tal.kaoyan:id/activity_register_email_edittext')
    register_btn=(By.ID,'com.tal.kaoyan:id/activity_register_register_btn')


    #完善信息列表元素
    perfectinfomation_school=(By.ID,'com.tal.kaoyan:id/perfectinfomation_edit_school_name')
    perfectinfomation_major=(By.ID,'com.tal.kaoyan:id/activity_perfectinfomation_major')
    perfectinfomation_goBtn=(By.ID,'com.tal.kaoyan:id/activity_perfectinfomation_goBtn')



    #院校列表元素
    forum_title=(By.ID,'com.tal.kaoyan:id/more_forum_title')
    university=(By.ID,'com.tal.kaoyan:id/university_search_item_name')


    #專業列表元素
    major_subject_title= (By.ID, 'com.tal.kaoyan:id/major_subject_title')
    major_group_title= (By.ID, 'com.tal.kaoyan:id/major_group_title')
    major_search_item_name= (By.ID, 'com.tal.kaoyan:id/major_search_item_name')

    # 個人中心元素
    username = (By.ID, 'com.tal.kaoyan:id/activity_usercenter_username')
    button_myself = (By.ID, 'com.tal.kaoyan:id/mainactivity_button_mysefl')


    def register_action(self,register_username,register_password,register_email):
        self.check_cancelBtn()
        self.check_skipBtn()

        logging.info('=========register_action===========')
        self.driver.find_element(*self.register_text).click()

        #頭像設置
        logging.info('set userheader')
        self.driver.find_element(*self.userheader).click()
        self.driver.find_elements(*self.item_image)[10].click()
        self.driver.find_element(*self.saveBtn).click()


        #用戶名密碼填寫
        logging.info('register username is %s' %register_username)
        self.driver.find_element(*self.register_username).send_keys(register_username)

        logging.info('register_password is %s' %register_password)
        self.driver.find_element(*self.register_password).send_keys(register_password)

        logging.info('register_email is %s' %register_email)
        self.driver.find_element(*self.register_email).send_keys(register_email)

        logging.info('click register button')
        self.driver.find_element(*self.register_btn).click()

        # 判斷是否進入到完善信息界面--注冊太頻繁會被限制無法進入該界面
        try:
            self.driver.find_element(*self.perfectinfomation_school)
        except NoSuchElementException:
            logging.error('register Fail!')
            self.getScreenShot('register Fail')
            return False
        else:
            self.add_register_info()
            #注冊結果判斷
            if self.check_registerStatus():
                return True
            else:
                return False


    def add_register_info(self):
        logging.info('===========add_register_info===========')

        # 院校選擇:上海——同濟大學
        logging.info("select school...")
        self.driver.find_element(*self.perfectinfomation_school).click()
        self.driver.find_elements(*self.forum_title)[1].click()
        self.driver.find_elements(*self.university)[1].click()

        #專業選擇:經濟學類-統計學-經濟統計學
        logging.info("select major...")
        self.driver.find_element(*self.perfectinfomation_major).click()
        self.driver.find_elements(*self.major_subject_title)[1].click()
        self.driver.find_elements(*self.major_group_title)[2].click()
        self.driver.find_elements(*self.major_search_item_name)[1].click()

        self.driver.find_element(*self.perfectinfomation_goBtn).click()

    def check_register_status(self):
        self.check_market_ad()
        logging.info('==========check_registerStatus===========')

        try:
            self.driver.find_element(*self.button_myself).click()
            self.driver.find_element(*self.username)
        except NoSuchElementException:
            logging.error('register Fail!')
            self.getScreenShot('register_Fail')
            return False
        else:
            logging.info('register success!')
            self.getScreenShot('register_success')
            return True


if __name__ == '__main__':
    driver=appium_desired()
    register=RegisterView(driver)

    username='zxw2018'+'FLY'+str(random.randint(1000,9000))
    password='zxw'+str(random.randint(1000,9000))
    email='51zxw'+str(random.randint(1000,9000))+'@163.com'

    register.register_action(username,password,email)

 

data數據封裝

使用背景

在實際項目過程中,我們的數據可能是存儲在一個數據文件中,如txt,excel、csv文件類型。我們可以封裝一些方法來讀取文件中的數據來實現數據驅動。

案例

將測試賬號存儲在account.csv文件,內容如下:

自學網2017

zxw2017

自學網2018

zxw2018

666

222

 
 
enumerate()簡介

enumerate()是python的內置函數

  • enumerate在字典上是枚舉、列舉的意思
  • 對於一個可迭代的(iterable)/可遍歷的對象(如列表、字符串),enumerate將其組成一個索引序列,利用它可以同時獲得索引和值
  • enumerate多用於在for循環中得到計數。
enumerate()使用

如果對一個列表,既要遍歷索引又要遍歷元素時,首先可以這樣寫:

list = ["", "", "一個", "測試","數據"]
    for i in range(len(list)):
        print(i,list[i])
>>>
0 這
12 一個
3 測試
4 數據

上述方法有些累贅,利用enumerate()會更加直接和優美:

list1 = ["", "", "一個", "測試","數據"]
    for index, item in enumerate(list1):
        print(index,item)
>>>
0 這
12 一個
3 測試
4 數據
數據讀取方法封裝
import csv

     def get_csv_data(csv_file,line):
        with open(csv_file, 'r', encoding='utf-8-sig') as file:
            reader=csv.reader(file)
            for index, row in enumerate(reader,1):
                if index == line:
                    return row

    csv_file='../data/account.csv'
    data=get_csv_data(csv_file,3)
    print(data)
utf-8與utf-8-sig兩種編碼格式的區別

UTF-8以字節為編碼單元,它的字節順序在所有系統中都是一樣的,沒有字節序的問題,也因此它實際上並不需要BOM(“ByteOrder Mark”)。但是UTF-8 with BOM即utf-8-sig需要提供BOM。

config文件配置

日志文件配置 log.config

[loggers]
keys=root,infoLogger

[logger_root]
level=DEBUG
handlers=consoleHandler,fileHandler

[logger_infoLogger]
handlers=consoleHandler,fileHandler
qualname=infoLogger
propagate=0

[handlers]
keys=consoleHandler,fileHandler

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=form02
args=(sys.stderr,)

[handler_fileHandler]
class=FileHandler
level=INFO
formatter=form01
args=('../logs/runlog.log', 'a')

[formatters]
keys=form01,form02

[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s

[formatter_form02]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s

測試用例封裝

1.測試用例執行開始結束操作封裝 myunit.py

import unittest
from common.desired_caps import appium_desired
import logging
from  time import sleep

class StartEnd(unittest.TestCase):

    def setUp(self):
        logging.info('======setUp=========')
        self.driver=appium_desired()


    def tearDown(self):
        logging.info('======tearDown=====')
        sleep(5)
        self.driver.close_app()

2.注冊用例:test_register.py

from common.myunit import StartEnd
from businessView.registerView import RegisterView
import logging
import random
import unittest


class RegisterTest(StartEnd):

    def test_user_register(self):
        logging.info('=========test_user_register======')
        r=RegisterView(self.driver)

        username = 'zxw2018' + 'FLY' + str(random.randint(1000, 9000))
        password = 'zxw' + str(random.randint(1000, 9000))
        email = '51zxw' + str(random.randint(1000, 9000)) + '@163.com'

        self.assertTrue(r.register_action(username, password, email))


if __name__ == '__main__':
    unittest.main()

3.登錄用例:test_login.py

from common.myunit import StartEnd
from businessView.loginView import LoginView
import unittest
import logging


class LoginTest(StartEnd):
    csv_file = '../data/account.csv'

    # @unittest.skip("test_login_zxw2017")
    def test_login_zxw2017(self):
        logging.info('==========test_login_zxw2017========')
        l=LoginView(self.driver)
        data = l.get_csv_data(self.csv_file,1)

        l.login_action(data[0],data[1])
        self.assertTrue(l.check_loginStatus())

    # @unittest.skip('skip test_login_zxw2018')
    def test_login_zxw2018(self):
        logging.info('=========test_login_zxw2018============')
        l=LoginView(self.driver)
        data = l.get_csv_data(self.csv_file,2)

        l.login_action(data[0],data[1])
        self.assertTrue(l.check_loginStatus())

    # @unittest.skip("test_login_erro")
    def test_login_erro(self):
        logging.info('=======test_login_erro=========')
        l=LoginView(self.driver)
        data = l.get_csv_data(self.csv_file, 3)

        l.login_action(data[0], data[1])
        self.assertTrue(l.check_loginStatus(),msg='login fail!')

if __name__ == '__main__':
    unittest.main()

執行測試用例&報告生成

BSTestRunner下載地址

run.py

import unittest
from BSTestRunner import BSTestRunner
import time
import logging

#指定測試用例和測試報告的路徑
test_dir = '../test_case'
report_dir = '../reports'

#加載測試用例
discover = unittest.defaultTestLoader.discover(test_dir, pattern='test_login.py')


#定義報告的文件格式
now = time.strftime("%Y-%m-%d %H_%M_%S")
report_name = report_dir + '/' + now + ' test_report.html'

#運行用例並生成測試報告
with open(report_name, 'wb') as f:
    runner = BSTestRunner(stream=f, title="Kyb Test Report", description="kyb Andriod app Test Report")
    logging.info("start run testcase...")
    runner.run(discover)

注意:

pattern參數可以控制運行不同模塊的用例,如下所示表示運行指定路徑以test開頭的模塊

discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')

Bat批處理執行測試

前面腳本開發階段我們都是使用pycharm IDE工具來運行腳本,但是當我們的腳本開發完成后,還每次打開IDE來執行自動化測試就不合理了,因為不僅每次打開比較麻煩,而且pycharm內存資源占用比較“感人”!這樣非常影響執行效率。 針對這種情況,我們可以使用cmd命令或者封裝為bat批處理腳本來運行。

啟動appium服務

start_appium.bat

@echo off
appium
pause

@echo off 為關閉“回顯”,讓命令行界面顯得整潔一些。

執行測試用例

run.bat

@echo off
d:
cd D:\kyb_testProject\test_run
C:\Python35\python.exe run.py
pause
注意事項:

1.執行之前需要在run.py腳本添加如下內容:

import sys
path='D:\\kyb_testProject\\'
sys.path.append(path)

項目在IDE(Pycharm)中運行和我們在cmd中運行的路徑是不一樣的,在pycharm中運行時, 會默認pycharm的目錄+我們的工程所在目錄為運行目錄。

而在cmd中運行時,會以我們的工程目錄所在目錄來運行。在import包時會首先從pythonPATH的環境變量中來查看包,如果沒有你的PYTHONPATH中所包含的目錄沒有工程目錄的根目錄,那么你在導入不是同一個目錄下的其他工程中的包時會出現import錯誤。

2.以上腳本編碼格式必須為utf-8

 

 

自動化測試平台

前面我們已經開發完測試腳本,也使用bat批處理來封裝了啟動Appium服務和運行測試用例。但是還是不夠自動化,比如我想每天下班時自動跑一下用例,或者當研發打了新包后自動開始運行測試腳本測試新包,那么該如實現呢?

持續集成(Continuous integration)

持續集成是一種軟件開發實踐,即團隊開發成員經常集成他們的工作,通過每個成員每天至少集成一次,也就意味着每天可能會發生多次集成。每次集成都通過自動化的構建(包括編譯,發布,自動化測試)來驗證,從而盡早地發現集成錯誤。

Jenkins簡介

Jenkins是一個開源軟件項目,是基於Java開發的一種持續集成工具,用於監控持續重復的工作,旨在提供一個開放易用的軟件平台,使軟件的持續集成變成可能。

下載與安裝

下載地址:https://jenkins.io/download/

下載后安裝到指定的路徑即可,默認啟動頁面為localhots:8080,如果8080端口被占用無法打開,可以進入到jenkins安裝目錄,找到jenkins.xml配置文件打開,修改如下代碼的端口號即可。

<arguments>-Xrs -Xmx256m -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle -jar "%BASE%\jenkins.war" --httpPort=8080 --webroot="%BASE%\war"</arguments>

構建觸發器

  1. 觸發遠程構建:如果您想通過訪問一個特殊的預定義URL來觸發新構建,請啟用此選項。
  2. Build after other projects are built:在其他項目觸發的時候觸發,里面有分為三種情況,也就是其他項目構建成功、失敗、或者不穩定的時候觸發項目;
  3. Build periodically 定時構建
  4. GitHub hook trigger for GITScm polling,根源Git的源碼更新來觸發構建
  1. Poll SCM:定時檢查源碼變更(根據SCM軟件的版本號),如果有更新就checkout最新code下來,然后執行構建動作。如下圖配置:
    */5 * * * * (每5分鍾檢查一次源碼變化)

    jenkins定時構建語法

    * * * * *

(五顆星,中間用空格隔開)

  • 第一個*表示分鍾,取值0~59
  • 第二個*表示小時,取值0~23
  • 第三個*表示一個月的第幾天,取值1~31
  • 第四個*表示第幾月,取值1~12
  • 第五個*表示一周中的第幾天,取值0~7,其中0和7代表的都是周日
使用案例

每天下午下班前18點定時構建一次

0 18 * * *

每天早上8點構建一次

0 8 * * *

30分鍾構建一次:

H/30 * * * *

補充資料:Python郵件發送

參考資料

  1. https://blog.csdn.net/vernice/article/details/46873169
  2. https://blog.csdn.net/churximi/article/details/51648388
  3. https://www.cnblogs.com/robert-zhang/p/9060365.html
  4. https://baike.baidu.com/item/Jenkins/10917210?fr=aladdin
  5. https://baike.baidu.com/item/持續集成/6250744
  6. https://www.cnblogs.com/caoj/p/7815820.html

 


免責聲明!

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



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