框架功能
?業(yè)務(wù)功能的封裝
?測(cè)試用例封裝
?測(cè)試包管理
?截圖處理
?斷言處理
?日志獲取
?測(cè)試報(bào)告生成
?數(shù)據(jù)驅(qū)動(dòng)
?數(shù)據(jù)配置
測(cè)試案例
測(cè)試環(huán)境
Mac
Appium v1.18.3
app軟件
華為mate30
覆蓋用例
1.登錄場(chǎng)景
2.下單場(chǎng)景
框架設(shè)計(jì)圖
代碼實(shí)現(xiàn)
driver配置封裝
kyb_caps.yaml 配置表
# 測(cè)試平臺(tái) iOS和Android
platformName: Android
# 連接模擬器
# 平臺(tái)版本
#platformVersion: 6.0.1
# 設(shè)備名字
deviceName: HUAIWEI Mate 30 Pro 5G
# 連接真機(jī)
platformVersion: '10.0'
uuid: *************3142
#便于后期維護(hù)渴庆,可以只修改appname
appname: ********debug.apk(包名)
# 支持中文輸入
unicodeKeyboard: True
# 重置輸入法到原有狀態(tài)
resetKeyboard: True
# 控制是否清除session信息
noReset: False
# app的包
appPackage: com.**********
# app的 activity
appActivity: com.**************.******Activity
# appium server的IP和port
ip: 127.0.0.1
port: 4723
desired_caps.py文件
from appium import webdriver
import yaml
import logging
import logging.config
import os
CON_LOG='../config/log.conf'
logging.config.fileConfig(CON_LOG)
logging=logging.getLogger()
def appium_desired():
? ? with open('../config/********caps.yaml', 'r', encoding='utf-8') as file:
? ? ? ? data = yaml.load(file,Loader = yaml.FullLoader)
? ? desired_caps={}
? ? desired_caps['platformName'] = data['platformName']
? ? desired_caps['platformVersion'] = data['platformVersion']
? ? desired_caps['deviceName'] = data['deviceName']
? ? desired_caps['uuid'] = data['uuid']
? ? base_dir = os.path.dirname(os.path.dirname(__file__))
? ? app_path = os.path.join(base_dir, 'appname', data['appname'])
? ? desired_caps['appname'] = app_path
? ? desired_caps['appPackage'] = data['appPackage']
? ? desired_caps['appActivity'] = data['appActivity']
? ? desired_caps['noReset'] = data['noReset']
? ? desired_caps['unicodeKeyboard'] = data['unicodeKeyboard']
? ? desired_caps['resetKeyboard'] = data['resetKeyboard']
? ? logging.info('start app...')
? ? # driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
? ? driver = webdriver.Remote('http://' + str(data['ip']) + ':' + str(data['port']) + '/wd/hub',desired_caps)
? ? return driver
if __name__ == '__main__':
? ? appium_desired()
相對(duì)路徑符號(hào)含義
“.”表示當(dāng)前目錄
“..” 表示當(dāng)前目錄的上一級(jí)目錄。
“./”表示當(dāng)前目錄下的某個(gè)文件或文件夾,視后面跟著的名字而定
“../”表示當(dāng)前目錄上一級(jí)目錄的文件或文件夾抢野,視后面跟著的名字而定团赁。
基類(lèi)封裝
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.baseViews import BaseView
from common.desired_caps import appium_desired
from selenium.common.exceptions import NoSuchElementException
import logging
from selenium.webdriver.common.by import By
import time,os
import csv
class Common(BaseView):
#當(dāng)前時(shí)間
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)
? ? print(image_file)
? ? logging.info('get %s screenshot' %module)
? ? self.driver.get_screenshot_as_file(image_file)
#讀取csv文件
def get_csv_data(self,csv_file,line):
? ? logging.info('========get_csv_data=========')
? ? 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
#獲取屏幕尺寸
def get_size(self):
? ? x =self.driver.get_window_size()['width']
? ? y =self.driver.get_window_size()['height']
? ? return x, y
if __name__ == '__main__':
? ? driver = appium_desired()
? ? com = Common(driver)
? ? com.swipeLeft()
? ? com.getScreenShot(str('startapp'))
業(yè)務(wù)模塊封裝
1.登錄頁(yè)面業(yè)務(wù)邏輯模塊
import logging
from common.commin_fun import Common,NoSuchElementException
from common.desired_caps import appium_desired
from selenium.webdriver.common.by import By
from page.loginpage import LoginPage
import time
#登錄
class LoginView(Common):
? ? # 跳轉(zhuǎn)登錄
? ? logBtn = (By.ID, '*********:id/t*******e')
? ? # 用戶(hù)名
? ? username_type = (By.XPATH,'/**************************ditText')
? ? # 密碼
? ? password_type = (By.XPATH,'/*****************itText')
? ? # 登錄按鈕
? ? loginBtn = (By.ID, 'com.**********:id/bt****gin')
? ? # homepage = (By.ID,'com.***********:id/iv**nt')
? ? tip_commit = (By.ID, 'com.***********:id/tv_****dits')
? ? def login_action(self,username,password):
? ? ? ? logging.info('click logBtn')
? ? ? ? self.driver.find_element(*self.logBtn).click()
? ? ? ? logging.info('click logBtn finished')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('=========login_action=======')
? ? ? ? logging.info('username is:%s' %username)
?? ? ? self.driver.find_element(*self.username_type).send_keys(username)
? ? ? ? logging.info('usernameinput finished')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('=========login_action=======')
? ? ? ? logging.info('password is:%s' %password)
?? ? ? self.driver.find_element(*self.password_type).send_keys(password)
? ? ? ? logging.info('passwordinput finished')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('click loginBtn')
? ? ? ? self.driver.find_element(*self.loginBtn).click()
? ? ? ? logging.info('login loginBtn finished')
? ? ? ? time.sleep(3)
? ? def check_account_alert(self):
? ? ? ? logging.info('============check_account_alert===========')
? ? ? ? try:
? ? ? ? ? ? element = self.driver.find_element(self.tip_commit)
? ? ? ? except NoSuchElementException:
? ? ? ? ? ? pass
? ? ? ? else:
? ? ? ? ? ? logging.info('Logged in')
? ? def check_login_staus(self):
? ? ? ? logging.info('=======check_login_staus========')
? ? ? ? # self.check_market_ad()
? ? ? ? self.check_account_alert()
? ? ? ? try:
? ? ? ? ? ? self.driver.find_element(*self.tip_commit)
? ? ? ? except NoSuchElementException:
? ? ? ? ? ? logging.error('login Fail')
? ? ? ? ? ? self.getScreenShot('login Fail')
? ? ? ? ? ? return False
? ? ? ? else:
? ? ? ? ? ? logging.info('===========login success==============')
? ? ? ? ? ? return True
if __name__ == '__main__':
? ? driver = appium_desired()
? ? l = LoginView(driver)
? ? l.login_action('1584********692','q******************/an******View')
? ? #跳過(guò)動(dòng)畫(huà)
? ? skip_animation = (By.ID,'com.*********:id/skip')
? ? #跳過(guò)彈窗
? ? skip_popup = (By.ID,'com.**********:id/tv_ok')
? ? #首頁(yè)
? ? homepage = (By.ID, 'com.*******:id/iv*****ent')
? ? bj_cbd = (By.ID,'com.*************:id/ite****mg')
? ? reserve_room = (By.XPATH,'/***********************out')
? ? #同意
? ? agree = (By.ID,'com.**********:id/c*********s')
? ? #提交訂單
? ? submit = (By.ID,'com.***************:id/b************it')
? ? #立即支付
? ? immediate_payment = (By.ID,'com.**************id/b*******y')
? ? #賬戶(hù)余額
? ? account_balance = (By.ID,'com.***************id/c***********nt')
? ? #確認(rèn)支付
? ? confirm_payment = (By.ID,'com.*********************id/b*******y')
? ? def Yizhan_xiadan(self):
? ? ? ? logging.info('click homepage')
? ? ? ? self.driver.find_element(*self.homepage).click()
? ? ? ? logging.info('login homepage finished')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('=========click? yizhan_homepage=========')
? ? ? ? self.driver.find_element(*self.yizhan_homepage).click()
? ? ? ? logging.info('==============click yizhan_homepage finished===============')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('=========click? skip_animation=========')
? ? ? ? self.driver.find_element(*self.skip_animation).click()
? ? ? ? logging.info('==============click skip_animation finished===============')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('=========click? skip_popup=========')
? ? ? ? self.driver.find_element(*self.skip_popup).click()
? ? ? ? logging.info('==============click skip_popup finished===============')
? ? ? ? time.sleep(3)
? ? ? ? logging.info('============swipe up=============')
? ? ? ? self.swipe(868,1735,868,757,500)
? ? ? ? logging.info('============swipe up? finished=============')
? ? ? ? logging.info('==============cilck? bj_cbd=================')
? ? ? ? self.driver.find_element(*self.bj_cbd).click()
? ? ? ? logging.info('==============bj_cbd? finished===============')
? ? ? ? logging.info('============swipe up two=============')
? ? ? ? self.swipe(835, 1857, 835, 776, 696)
? ? ? ? logging.info('============swipe up two finished=============')
? ? ? ? logging.info('==================click reserve_room==============')
? ? ? ? self.driver.find_element(*self.reserve_room).click()
? ? ? ? logging.info('============reserve_room finished=============')
? ? ? ? logging.info('==================click agree==============')
? ? ? ? self.driver.find_element(*self.agree).click()
? ? ? ? logging.info('============agree finished=============')
? ? ? ? logging.info('==================click submit==============')
? ? ? ? self.driver.find_element(*self.submit).click()
? ? ? ? logging.info('============submit finished=============')
? ? ? ? logging.info('==================click immediate_payment==============')
? ? ? ? self.driver.find_element(*self.immediate_payment).click()
? ? ? ? logging.info('============immediate_payment finished=============')
? ? ? ? logging.info('==================click account_balance==============')
? ? ? ? self.driver.find_element(*self.account_balance).click()
? ? ? ? logging.info('============account_balance finished=============')
? ? ? ? logging.info('==================click confirm_payment==============')
? ? ? ? self.driver.find_element(*self.confirm_payment).click()
? ? ? ? logging.info('============confirm_payment finished=============')
data數(shù)據(jù)封裝
使用背景
在實(shí)際項(xiàng)目過(guò)程中瞎暑,我們的數(shù)據(jù)可能是存儲(chǔ)在一個(gè)數(shù)據(jù)文件中匕坯,如txt,excel掠抬、csv文件類(lèi)型氮块。我們可以封裝一些方法來(lái)讀取文件中的數(shù)據(jù)來(lái)實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)绍载。
案例
將測(cè)試賬號(hào)存儲(chǔ)在account.csv文件,內(nèi)容如下:
130*******745q******
enumerate()簡(jiǎn)介
enumerate()是python的內(nèi)置函數(shù)
enumerate在字典上是枚舉滔蝉、列舉的意思
對(duì)于一個(gè)可迭代的(iterable)/可遍歷的對(duì)象(如列表击儡、字符串),enumerate將其組成一個(gè)索引序列蝠引,利用它可以同時(shí)獲得索引和值
enumerate多用于在for循環(huán)中得到計(jì)數(shù)阳谍。
enumerate()使用
如果對(duì)一個(gè)列表,既要遍歷索引又要遍歷元素時(shí)立肘,首先可以這樣寫(xiě):
list= ["這", "是", "一個(gè)", "測(cè)試","數(shù)據(jù)"]
? ? foriinrange(len(list)):
? ? ? ? print(i,list[i])
>>>
0 這
1 是
2 一個(gè)
3 測(cè)試
4 數(shù)據(jù)
上述方法有些累贅边坤,利用enumerate()會(huì)更加直接和優(yōu)美:
list1 = ["這", "是", "一個(gè)", "測(cè)試","數(shù)據(jù)"]
? ? forindex,iteminenumerate(list1):
? ? ? ? print(index,item)
>>>
0 這
1 是
2 一個(gè)
3 測(cè)試
4 數(shù)據(jù)
數(shù)據(jù)讀取方法封裝
import csv
def get_csv_data(self,csv_file,line):
? ? logging.info('========get_csv_data=========')
? ? 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兩種編碼格式的區(qū)別
UTF-8以字節(jié)為編碼單元,它的字節(jié)順序在所有系統(tǒng)中都是一樣的谅年,沒(méi)有字節(jié)序的問(wèn)題茧痒,也因此它實(shí)際上并不需要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
測(cè)試用例封裝
1.測(cè)試用例執(zhí)行開(kāi)始結(jié)束操作封裝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('=========start============')
? ? ? ? self.driver = appium_desired()
? ? def tearDown(self):
? ? ? ? logging.info('===========end==========')
? ? ? ? sleep(5)
? ? ? ? self.driver.close_app()
2.登錄和下單用例:test_login.py
from common.myunit import StartEnd
from businessView.loginVIew import LoginView
from businessView.homePage import HomePage
from businessView.misu_xiadan import YizhanXiadan
import logging,unittest
from BSTestRunner import BSTestRunner
import time
#登錄
class LoginYest(StartEnd):
? ? csv_file ='../data/account.csv'
? ? def test_login_cg(self):
? ? ? ? logging.info('===============test_page===============')
? ? ? ? h = HomePage(self.driver)
? ? ? ? l = LoginView(self.driver)
? ? ? ? y = YizhanXiadan(self.driver)
? ? ? ? data = l.get_csv_data(self.csv_file, 1)
? ? ? ? h.get_into()
? ? ? ? l.login_action(data[0], data[1])
? ? ? ? self.assertTrue(l.check_login_staus(), msg=('login fail!!!'))
? ? ? ? time.sleep(3)
? ? ? ? y.Yizhan_xiadan()
if __name__ == '__main__':
? ? unittest.main()
執(zhí)行測(cè)試用例&報(bào)告生成
run.py文件
from BSTestRunner import BSTestRunner
import unittest
import time,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='***測(cè)試報(bào)告',description='*******登錄下單流程')
? ? logging.info('start run rest case')
? ? runner.run(discover)
注意:
pattern參數(shù)可以控制運(yùn)行不同模塊的用例旺订,如下所示表示運(yùn)行指定路徑以test開(kāi)頭的模塊
discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')