構造紹介
以前AndroidUIテストを共有していましたが、データとコードの分離に気づかず、メンテナンスコストが高かったので、最近最適化しました。
記事を読みたくない場合は、Githubに直接アクセスして、レンガを作ることを歓迎します。
一般的な構造は次のとおりです。
Structure.png
-
testyaml管理のユースケース、モジュールごとに1つのフォルダーでデータとコードの分離を実現
-
public設定ファイルの読み取り、appiumサービスの開始、Yamlファイルの読み取り、ログ形式の定義などのパブリックファイルを保存します。
-
ページには、モジュールごとに1つのフォルダーという、テストケースの最小セットが格納されます。
-
結果テストレポートと失敗したスクリーンショットを保存します
report.png
-
ログはログを保存します
logs.png
logdetail.png
- テストケースはテストケースを保存します
- runtest.pyはすべてのテストケースを実行します
yamlフォーマットの紹介
最初にyamlファイルの形式を見てください。以前にyaml文法学習についての小さな記事も書きました。
テストケースの部分に焦点が当てられています。
-
element_info:要素情報の配置
-
find_type:サブ、id、xpath、text、ids
-
operator_type:click、sendkeys、back、swipe_upは「back」と「return」です。当面は4つあります
上記の3つが必要です、operate_typeが必要です!!!!!!
-
send_content:send_keysのときに使用されます
-
インデックス:IDの場合に使用
-
時間:リターン時間またはアップスライド時間
testinfo:
- id: cm001
title: 新增终端门店
execute: 1
testcase:
-
element_info: 客户
find_type: text
operate_type: click
-
element_info: com.fiberhome.waiqin365.client:id/cm_topbar_tv_right
find_type: id
operate_type: click
-
element_info: com.fiberhome.waiqin365.client:id/custview_id_singletv_inputtext
find_type: ids
operate_type: send_keys
send_content: auto0205
index: 0
-
element_info:
find_type:
operate_type: swipe_up
times: 1
-
element_info: 提交
find_type: text
operate_type: click
-
element_info:
find_type:
operate_type: back
times: 1
コード部分
パブリックセクション
個人的には、家を建てるのと同じ公共部分がコアだと思います。公共部分ができたら、後で呼んでください。家を建てて棚を設置し、後ろにレンガやタイルを追加します。
構成ファイルreadconfig.pyを読み取り、
ログ形式logs.pyを設定し
て、デバイスGetDevices.pyを取得します。
これらの一般的なものは導入されません。
- yamlファイルを読み取るGetYaml.pyは
主にyamlファイルを読み取るために使用されます
#coding=utf-8
#author='Shichao-Dong'
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import yaml
import codecs
class getyaml:
def __init__(self,path):
self.path = path
def getYaml(self):
'''
读取yaml文件
:param path: 文件路径
:return:
'''
try:
f = open(self.path)
data =yaml.load(f)
f.close()
return data
except Exception:
print(u"未找到yaml文件")
def alldata(self):
data =self.getYaml()
return data
def caselen(self):
data = self.alldata()
length = len(data['testcase'])
return length
def get_elementinfo(self,i):
data = self.alldata()
# print data['testcase'][i]['element_info']
return data['testcase'][i]['element_info']
def get_findtype(self,i):
data = self.alldata()
# print data['testcase'][i]['find_type']
return data['testcase'][i]['find_type']
def get_operate_type(self,i):
data = self.alldata()
# print data['testcase'][i]['operate_type']
return data['testcase'][i]['operate_type']
def get_index(self,i):
data = self.alldata()
if self.get_findtype(i)=='ids':
return data['testcase'][i]['index']
else:
pass
def get_send_content(self,i):
data = self.alldata()
# print data['testcase'][i]['send_content']
if self.get_operate_type(i) == 'send_keys':
return data['testcase'][i]['send_content']
else:
pass
def get_backtimes(self,i):
data = self.alldata()
if self.get_operate_type(i)=='back' or self.get_operate_type(i)=='swipe_up':
return data['testcase'][i]['times']
else:
pass
def get_title(self):
data = self.alldata()
# print data['testinfo'][0]['title']
return data['testinfo'][0]['title']
- appiumサービスを開始しますStartAppiumServer.pyは
主にappiumを起動し、以下のドライバーで必要なポートポートを返します
#coding=utf-8
#author='Shichao-Dong'
from logs import log
import random,time
import platform
import os
from GetDevices import devices
log = log()
dev = devices().get_deviceName()
class Sp:
def __init__(self, device):
self.device = device
def __start_driver(self, aport, bpport):
"""
:return:
"""
if platform.system() == 'Windows':
import subprocess
subprocess.Popen("appium -p %s -bp %s -U %s" %
(aport, bpport, self.device), shell=True)
def start_appium(self):
"""
启动appium
p:appium port
bp:bootstrap port
:return: 返回appium端口参数
"""
aport = random.randint(4700, 4900)
bpport = random.randint(4700, 4900)
self.__start_driver(aport, bpport)
log.info(
'start appium :p %s bp %s device:%s' %
(aport, bpport, self.device))
time.sleep(10)
return aport
def main(self):
"""
:return: 启动appium
"""
return self.start_appium()
def stop_appium(self):
'''
停止appium
:return:
'''
if platform.system() == 'Windows':
os.popen("taskkill /f /im node.exe")
if __name__ == '__main__':
s = Sp(dev)
s.main()
- ドライバーGetDriver.pyplatformName
、deviceName、appPackage、appActivityを取得します。これらのアンインストール構成ファイルconfig.iniファイルは、readconfig.pyファイルから直接読み取ることができます。
appium_portにはStartAppiumServer.pyファイルが返されます
s = Sp(deviceName)
appium_port = s.main()
def mydriver():
desired_caps = {
'platformName':platformName,'deviceName':deviceName, 'platformVersion':platformVersion,
'appPackage':appPackage,'appActivity':appActivity,
'unicodeKeyboard':True,'resetKeyboard':True,'noReset':True
}
try:
driver = webdriver.Remote('http://127.0.0.1:%s/wd/hub'%appium_port,desired_caps)
time.sleep(4)
log.info('获取driver成功')
return driver
except WebDriverException:
print 'No driver'
if __name__ == "__main__":
mydriver()
- find、BaseOperate.pyなどの再パッケージコマンドには、
主に、スライドアップ、リターン、検索などの基本的な操作が含まれています。
#coding=utf-8
#author='Shichao-Dong'
from selenium.webdriver.support.ui import WebDriverWait
from logs import log
import os
import time
'''
一些基础操作:滑动、截图、点击页面元素等
'''
class BaseOperate:
def __init__(self,driver):
self.driver = driver
def back(self):
'''
返回键
:return:
'''
os.popen("adb shell input keyevent 4")
def get_window_size(self):
'''
获取屏幕大小
:return: windowsize
'''
global windowSize
windowSize = self.driver.get_window_size()
return windowSize
def swipe_up(self):
'''
向上滑动
:return:
'''
windowsSize = self.get_window_size()
width = windowsSize.get("width")
height = windowsSize.get("height")
self.driver.swipe(width/2, height*3/4, width/2, height/4, 1000)
def screenshot(self):
now=time.strftime("%y%m%d-%H-%M-%S")
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
screenshoot_path = PATH('../results/screenshoot/')
self.driver.get_screenshot_as_file(screenshoot_path+now+'.png')
def find_id(self,id):
'''
寻找元素
:return:
'''
exsit = self.driver.find_element_by_id(id)
if exsit :
return True
else:
return False
def find_name(self,name):
'''
判断页面是否存在某个元素
:param name: text
:return:
'''
findname = "//*[@text='%s']"%(name)
exsit = self.driver.find_element_by_xpath(findname)
if exsit :
return True
else:
return False
def get_name(self,name):
'''
定位页面text元素
:param name:
:return:
'''
# element = driver.find_element_by_name(name)
# return element
findname = "//*[@text='%s']"%(name)
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(findname))
# element = self.driver.find_element_by_xpath(findname)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(name)
def get_id(self,id):
'''
定位页面resouce-id元素
:param id:
:return:
'''
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_id(id))
# element = self.driver.find_element_by_id(id)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(id)
def get_xpath(self,xpath):
'''
定位页面xpath元素
:param id:
:return:
'''
try:
element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(xpath))
# element = self.driver.find_element_by_xpath(xpath)
self.driver.implicitly_wait(2)
return element
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(xpath)
def get_ids(self,id):
'''
定位页面resouce-id元素组
:param id:
:return:列表
'''
try:
# elements = self.driver.find_elements_by_id(id)
elements = WebDriverWait(self.driver, 10).until(lambda x: x.find_elements_by_id(id))
self.driver.implicitly_wait(2)
return elements
except:
self.screenshot()
log.error('未定位到元素:'+'%s')%(id)
def page(self,name):
'''
返回至指定页面
:return:
'''
i=0
while i<10:
i=i+1
try:
findname = "//*[@text='%s']"%(name)
self.driver.find_element_by_xpath(findname)
self.driver.implicitly_wait(2)
break
except :
os.popen("adb shell input keyevent 4")
try:
findname = "//*[@text='确定']"
self.driver.find_element_by_xpath(findname).click()
self.driver.implicitly_wait(2)
except:
os.popen("adb shell input keyevent 4")
try:
self.driver.find_element_by_xpath("//*[@text='工作台']")
self.driver.implicitly_wait(2)
break
except:
os.popen("adb shell input keyevent 4")
- Operate.py
が最も重要なステップだと思います。その後にページはありません。このファイルは、主に読み取りyamlファイルに基づいてテストのために呼び出され、次にif ... else ...の判断に基づいて、それぞれ対応します。対応するopera_typeへクリック、sendkeysおよびその他の操作
#coding=utf-8
#author='Shichao-Dong'
from GetYaml import getyaml
from BaseOperate import BaseOperate
class Operate:
def __init__(self,path,driver):
self.path = path
self.driver = driver
self.yaml = getyaml(self.path)
self.baseoperate=BaseOperate(driver)
def check_operate_type(self):
'''
读取yaml信息并执行
element_info:定位元素信息
find_type:属性,id、xpath、text、ids
operate_type: click、sendkeys、back、swipe_up 为back就是返回,暂时就三种
上面三个必填,operate_type必填!!!!!!
send_content:send_keys 时用到
index:ids时用到
times:
:return:
'''
for i in range(self.yaml.caselen()):
if self.yaml.get_operate_type(i) == 'click':
if self.yaml.get_findtype(i) == 'text':
self.baseoperate.get_name(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'id':
self.baseoperate.get_id(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'xpath':
self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).click()
elif self.yaml.get_findtype(i) == 'ids':
self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].click()
elif self.yaml.get_operate_type(i) == 'send_keys':
if self.yaml.get_findtype(i) == 'text':
self.baseoperate.get_name(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'id':
self.baseoperate.get_id(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'xpath':
self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_findtype(i) == 'ids':
self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].send_keys(self.yaml.get_send_content(i))
elif self.yaml.get_operate_type(i) == 'back':
for n in range(self.yaml.get_backtimes(i)):
self.baseoperate.back()
elif self.yaml.get_operate_type(i) == 'swipe_up':
for n in range(self.yaml.get_backtimes(i)):
self.baseoperate.swipe_up()
def back_home(self):
'''
返回至工作台
:return:
'''
self.baseoperate.page('工作台')
コードには共通部分がたくさんありますが、このフレームワークを書くときは、ほとんどのエネルギーがこの部分に費やされているので、個人的には勉強する価値があると感じています。
ページセクション
ページ部分は、1つのモジュールと1つのフォルダーの最小のユースケースセットです。顧客を例にとると、
現在、追加用と並べ替え用の2つのユースケースが作成されています。ファイルは次のとおりです。
file.png
コードは次のとおりで、非常に簡潔です。
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import codecs,os
from public.Operate import Operate
from public.GetYaml import getyaml
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
yamlpath = PATH("../../testyaml/cm/cm-001addcm.yaml")
class AddcmPage:
def __init__(self,driver):
self.path = yamlpath
self.driver = driver
self.operate = Operate(self.path,self.driver)
def operateap(self):
self.operate.check_operate_type()
def home(self):
self.operate.back_home()
ユースケースを実行する
このパートでは、unittestを使用してすべてのテストケースを実行し、レポートを生成します。
モジュールごとに1つのユースケース、例として顧客を取り上げます:CmTest.py
from page.cm.CmAddcmPage import AddcmPage
from page.cm.CmSortcmPage import SortcmPage
from public.GetDriver import mydriver
driver = mydriver()
import unittest,time
class Cm(unittest.TestCase):
def test_001addcm(self):
'''
新增客户
:return:
'''
add = AddcmPage(driver)
add.operateap()
add.home()
def test_002sortcm(self):
'''
客户排序
:return:
'''
sort = SortcmPage(driver)
sort.sortlist()
sort.home()
def test_999close(self):
driver.quit()
time.sleep(10)
if __name__ == "__main__":
unittest.main()
最初にページレイヤーから実行する必要のあるすべてのユースケースをインポートしてから、unittestで実行します。
すべてのテストケースを実行する場合は、runtest.pyを使用する必要があります
import time,os
import unittest
import HTMLTestRunner
from testcase.CmTest import Cm
def testsuit():
suite = unittest.TestSuite()
suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Cm),
])
# runner = unittest.TextTestRunner(verbosity=2)
# runner.run(suite)
now=time.strftime("%y-%m-%d-%H-%M-%S")
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
dirpath = PATH("./results/waiqin365-")
filename=dirpath + now +'result.html'
fp=open(filename,'wb')
runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title='waiqin365 6.0.6beta test result',description=u'result:')
runner.run(suite)
fp.close()
if __name__ =="__main__":
testsuit()
ここでの考え方は似ています。最初にインポートしてからスイートをロードするだけです。
総括する
当分の間、データとユースケースの分離は達成されましたが、yamlの書き込み要件は比較的高く、誤ってフォーマットすることはできません。
同時に、次のような最適化できる他の領域がいくつかあります。
- ポップアップの判断
- 切断メカニズム後に再接続します
- 失敗後にメカニズムを再実行する