導入如下包
框架背景
前面我們已經學習了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)
相對路徑符號含義
- “.”表示當前目錄
- “..” 表示當前目錄的上一級目錄。
- “./”表示當前目錄下的某個文件或文件夾,視后面跟着的名字而定
- “../”表示當前目錄上一級目錄的文件或文件夾,視后面跟着的名字而定。
基類封裝
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.登錄頁面業務邏輯模塊
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()
注冊頁面業務邏輯封裝
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 這 1 是 2 一個 3 測試 4 數據
上述方法有些累贅,利用enumerate()會更加直接和優美:
list1 = ["這", "是", "一個", "測試","數據"] for index, item in enumerate(list1): print(index,item) >>> 0 這 1 是 2 一個 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()
執行測試用例&報告生成
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>
構建觸發器
- 觸發遠程構建:如果您想通過訪問一個特殊的預定義URL來觸發新構建,請啟用此選項。
- Build after other projects are built:在其他項目觸發的時候觸發,里面有分為三種情況,也就是其他項目構建成功、失敗、或者不穩定的時候觸發項目;
- Build periodically 定時構建
- GitHub hook trigger for GITScm polling,根源Git的源碼更新來觸發構建
- 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郵件發送
參考資料
- https://blog.csdn.net/vernice/article/details/46873169
- https://blog.csdn.net/churximi/article/details/51648388
- https://www.cnblogs.com/robert-zhang/p/9060365.html
- https://baike.baidu.com/item/Jenkins/10917210?fr=aladdin
- https://baike.baidu.com/item/持續集成/6250744
- https://www.cnblogs.com/caoj/p/7815820.html