Appium+Python+pytest自動(dòng)化測(cè)試框架的實(shí)戰(zhàn)
菜鳥一枚,寫的不好勿噴,大家一起學(xué)習(xí)
先簡(jiǎn)單介紹一下目錄,再貼一些代碼,代碼里有注釋
Basic目錄下寫的是一些公共的方法,Data目錄下寫的是測(cè)試數(shù)據(jù),image存的是測(cè)試失敗截圖,Log日志文件,Page測(cè)試的定位元素,report測(cè)試報(bào)告,Test測(cè)試用例,pytest.ini是pytest啟動(dòng)配置文件,requirements.txt需要安裝的py模塊,run.py運(yùn)行文件
Basic/base.py
里面封裝了 一些方法,元素的點(diǎn)擊,輸入,查找,還有一些自己需要的公共方法也封裝在里面,如果你們有別的需要可以自己封裝調(diào)用
# coding=utf-8 import random import allure import pymysql import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from Basic import Log import os log = Log.MyLog() class Base(object): def __init__(self, driver): self.driver = driver # 自定義一個(gè)元素查找方法 def find_element(self, feature,timeout=5, poll=1.0): # feature = By.XPATH,"http://*[@text='顯示']" """ 依據(jù)用戶傳入的元素信息特征,然后返回當(dāng)前用戶想要查找元素 :param feature: 元組類型,包含用戶希望的查找方式,及該方式對(duì)應(yīng)的值 :return: 返回當(dāng)前用戶查找的元素 """ by = feature[0] value = feature[1] wait = WebDriverWait(self.driver, timeout, poll) if by == By.XPATH: # print( "說明了用戶想要使用 xpath 路徑的方式來獲取元素" ) value = self.make_xpath(value) return wait.until(lambda x: x.find_element(by,value)) def find_elements(self, feature): wait = WebDriverWait(self.driver, 5, 1) return wait.until(lambda x: x.find_elements(feature[0], feature[1])) def click_element(self, loc): ''' 封裝點(diǎn)擊操作函數(shù) ''' self.find_element(loc).click() def input_text(self, loc, text): ''' 封裝輸入操作函數(shù) ''' self.fm = self.find_element(loc) self.fm.clear() # 需要先清空輸入框,防止有默認(rèn)內(nèi)容 self.fm.send_keys(text) # 自定義了一個(gè)可以自動(dòng)幫我們拼接 xpath 路徑的工具函數(shù) def make_xpath(self, feature): start_path = "http://*[" end_path = "]" res_path = "" if isinstance(feature, str): # 如果是字符串 我們不能直接上來就拆我們可以判斷一下它是否是默認(rèn)正確的 xpath 寫法 if feature.startswith("http://*["): return feature # 如果用戶輸入的是字符串,那么我們就拆成列表再次進(jìn)行判斷 split_list = feature.split(",") if len(split_list) == 2: # //*[contains(@text,'設(shè)')] res_path = "%scontains(@%s,'%s')%s" % (start_path, split_list[0], split_list[1], end_path) elif len(split_list) == 3: # //[@text='設(shè)置'] res_path = "%s@%s='%s'%s" % (start_path, split_list[0], split_list[1], end_path) else: print("請(qǐng)按規(guī)則使用") elif isinstance(feature, tuple): for item in feature: # 默認(rèn)用戶在元組當(dāng)中定義的數(shù)據(jù)都是字符串 split_list2 = item.split(',') if len(split_list2) == 2: res_path += "contains(@%s,'%s') and " % (split_list2[0], split_list2[1]) elif len(split_list2) == 3: res_path += "@%s='%s' and " % (split_list2[0], split_list2[1]) else: print("請(qǐng)按規(guī)則使用") andIndex = res_path.rfind(" and") res_path = res_path[0:andIndex] res_path = start_path + res_path + end_path else: print("請(qǐng)按規(guī)則使用") return res_path def assert_ele_in(self, text, element): ''' 封裝斷言操作函數(shù) ''' try: assert text in self.find_element(element).text assert 0 except Exception: assert 1 def get_assert_text(self, element): ele = self.find_element(element, timeout=5, poll=0.1) return ele.text # 自定義一個(gè)獲取 toast內(nèi)容的方法 def get_toast_content(self, message): tmp_feature = By.XPATH, "http://*[contains(@text,'%s')]" % message ele = self.find_element(tmp_feature) return ele.text # 自定義一個(gè)工具函數(shù),可以接收用戶傳遞的部分 toast 信息,然后返回一個(gè)布爾值,來告訴 # 用戶,目標(biāo) toast 到底是否存在 def is_toast_exist(self, mes): # 拿著用戶傳過來的 message 去判斷一下包含該內(nèi)容的 toast 到底是否存在。 try: self.get_toast_content(mes) return True except Exception: # 如果目標(biāo) toast 不存在那么就說明我們的實(shí)際結(jié)果和預(yù)期結(jié)果不一樣 # 因此我們想要的是斷言失敗 return False def get_mysql(self, table, value): '''連接數(shù)據(jù)庫''' # 打開數(shù)據(jù)庫連接 db = pymysql.connect(host='', port=, db=, user='', passwd='', charset='utf8') # 使用 cursor() 方法創(chuàng)建一個(gè)游標(biāo)對(duì)象 cursor cursor = db.cursor() try: # 使用 execute() 方法執(zhí)行 SQL 查詢 cursor.execute(value) db.commit() except Exception as e: print(e) db.rollback() # 使用 fetchone() 方法獲取單條數(shù)據(jù). data = cursor.fetchone() # 關(guān)閉數(shù)據(jù)庫連接 db.close() return data def get_xpath(self, value): '''封裝獲取xpath方法''' text = By.XPATH, '//*[@text="%s"]' % value return text # 自定義一個(gè)獲取當(dāng)前設(shè)備尺寸的功能 def get_device_size(self): x = self.driver.get_window_size()["width"] y = self.driver.get_window_size()["height"] return x, y # 自定義一個(gè)功能,可以實(shí)現(xiàn)向左滑屏操作。 def swipe_left(self): start_x = self.get_device_size()[0] * 0.9 start_y = self.get_device_size()[1] * 0.5 end_x = self.get_device_size()[0] * 0.4 end_y = self.get_device_size()[1] * 0.5 self.driver.swipe(start_x, start_y, end_x, end_y) # 自定義一個(gè)功能,可以實(shí)現(xiàn)向上滑屏操作。 def swipe_up(self): start_x = self.get_device_size()[0] * 1/2 start_y = self.get_device_size()[1] * 1/2 end_x = self.get_device_size()[0] * 1/2 end_y = self.get_device_size()[1] * 1/7 self.driver.swipe(start_x, start_y, end_x, end_y, 500) # 切換到微信 def switch_weixxin(self): self.driver.start_activity("com.tencent.mm", ".ui.LauncherUI") # 切換到醫(yī)生端 def switch_doctor(self): self.driver.start_activity("com.rjjk_doctor", ".MainActivity") # 切換到銷售端 def switch_sale(self): self.driver.start_activity("com.rjjk_sales", ".MainActivity") def switch_webview(self): # 切換到webview print(self.driver.contexts) time.sleep(5) self.driver.switch_to.context("WEBVIEW_com.tencent.mm:tools") print("切換成功") time.sleep(3) # 自定義根據(jù)坐標(biāo)定位 def taptest(self, a, b): # 設(shè)定系數(shù),控件在當(dāng)前手機(jī)的坐標(biāo)位置除以當(dāng)前手機(jī)的最大坐標(biāo)就是相對(duì)的系數(shù)了 # 獲取當(dāng)前手機(jī)屏幕大小X,Y X = self.driver.get_window_size()['width'] Y = self.driver.get_window_size()['height'] # 屏幕坐標(biāo)乘以系數(shù)即為用戶要點(diǎn)擊位置的具體坐標(biāo) self.driver.tap([(a * X, b * Y)]) # 自定義截圖函數(shù) def take_screenShot(self): ''' 測(cè)試失敗截圖,并把截圖展示到allure報(bào)告中 ''' tm = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(time.time())) self.driver.get_screenshot_as_file( os.getcwd() + os.sep + "image/%s.png" % tm) allure.attach.file(os.getcwd() + os.sep + "image/%s.png" % tm, attachment_type=allure.attachment_type.PNG) # 自定義隨機(jī)生成11位手機(jī)號(hào) def create_phone(self): # 第二位數(shù)字 second = [3, 4, 5, 7, 8][random.randint(0, 4)] # 第三位數(shù)字 third = { 3: random.randint(0, 9), 4: [5, 7, 9][random.randint(0, 2)], 5: [i for i in range(10) if i != 4][random.randint(0, 8)], 7: [i for i in range(10) if i not in [4, 9]][random.randint(0, 7)], 8: random.randint(0, 9), }[second] # 最后八位數(shù)字 suffix = random.randint(9999999, 100000000) # 拼接手機(jī)號(hào) return "1{}{}{}".format(second, third, suffix)
Basic/deiver.py
APP啟動(dòng)的前置條件,一個(gè)是普通的app,一個(gè)是微信公眾號(hào),配置微信公眾號(hào)自動(dòng)化測(cè)試和一般的APP是有點(diǎn)區(qū)別的,微信需要切換webview才能定位到公眾號(hào)
from appium import webdriver def init_driver(): desired_caps = {} # 手機(jī) 系統(tǒng)信息 desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '9' # 設(shè)備號(hào) desired_caps['deviceName'] = 'emulator-5554' # 包名 desired_caps['appPackage'] = '' # 啟動(dòng)名 desired_caps['appActivity'] = '' desired_caps['automationName'] = 'Uiautomator2' # 允許輸入中文 desired_caps['unicodeKeyboard'] = True desired_caps['resetKeyboard'] = True desired_caps['autoGrantPermissions'] = True desired_caps['noReset'] = False # 手機(jī)驅(qū)動(dòng)對(duì)象 driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) return driver def driver_weixin(): desired_caps = {} # 手機(jī) 系統(tǒng)信息 desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '9' # 設(shè)備號(hào) desired_caps['deviceName'] = '' # 包名 desired_caps['appPackage'] = 'com.tencent.mm' # 啟動(dòng)名 desired_caps['appActivity'] = '.ui.LauncherUI' # desired_caps['automationName'] = 'Uiautomator2' # 允許輸入中文 desired_caps['unicodeKeyboard'] = True desired_caps['resetKeyboard'] = True desired_caps['noReset'] = True # desired_caps["newCommandTimeout"] = 30 # desired_caps['fullReset'] = 'false' # desired_caps['newCommandTimeout'] = 10 # desired_caps['recreateChromeDriverSessions'] = True desired_caps['chromeOptions'] = {'androidProcess': 'com.tencent.mm:tools'} # 手機(jī)驅(qū)動(dòng)對(duì)象 driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) return driver
Basic/get_data.py
這是獲取測(cè)試數(shù)據(jù)的方法
import os import yaml def getData(funcname, file): PATH = os.getcwd() + os.sep with open(PATH + 'Data/' + file + '.yaml', 'r', encoding="utf8") as f: data = yaml.load(f, Loader=yaml.FullLoader) # 1 先將我們獲取到的所有數(shù)據(jù)都存放在一個(gè)變量當(dāng)中 tmpdata = data[funcname] # 2 所以此時(shí)我們需要使用循環(huán)走進(jìn)它的內(nèi)心。 res_arr = list() for value in tmpdata.values(): tmp_arr = list() for j in value.values(): tmp_arr.append(j) res_arr.append(tmp_arr) return res_arr
Basic/Log.py
日志文件,不多介紹
# -*- coding: utf-8 -*- """ 封裝log方法 """ import logging import os import time LEVELS = { 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL } logger = logging.getLogger() level = 'default' def create_file(filename): path = filename[0:filename.rfind('/')] if not os.path.isdir(path): os.makedirs(path) if not os.path.isfile(filename): fd = open(filename, mode='w', encoding='utf-8') fd.close() else: pass def set_handler(levels): if levels == 'error': logger.addHandler(MyLog.err_handler) logger.addHandler(MyLog.handler) def remove_handler(levels): if levels == 'error': logger.removeHandler(MyLog.err_handler) logger.removeHandler(MyLog.handler) def get_current_time(): return time.strftime(MyLog.date, time.localtime(time.time())) class MyLog: path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) log_file = path+'/Log/log.log' err_file = path+'/Log/err.log' logger.setLevel(LEVELS.get(level, logging.NOTSET)) create_file(log_file) create_file(err_file) date = '%Y-%m-%d %H:%M:%S' handler = logging.FileHandler(log_file, encoding='utf-8') err_handler = logging.FileHandler(err_file, encoding='utf-8') @staticmethod def debug(log_meg): set_handler('debug') logger.debug("[DEBUG " + get_current_time() + "]" + log_meg) remove_handler('debug') @staticmethod def info(log_meg): set_handler('info') logger.info("[INFO " + get_current_time() + "]" + log_meg) remove_handler('info') @staticmethod def warning(log_meg): set_handler('warning') logger.warning("[WARNING " + get_current_time() + "]" + log_meg) remove_handler('warning') @staticmethod def error(log_meg): set_handler('error') logger.error("[ERROR " + get_current_time() + "]" + log_meg) remove_handler('error') @staticmethod def critical(log_meg): set_handler('critical') logger.error("[CRITICAL " + get_current_time() + "]" + log_meg) remove_handler('critical') if __name__ == "__main__": MyLog.debug("This is debug message") MyLog.info("This is info message") MyLog.warning("This is warning message") MyLog.error("This is error") MyLog.critical("This is critical message")
Basic/Shell.py
執(zhí)行shell語句方法
# -*- coding: utf-8 -*- # @Time : 2018/8/1 下午2:54 # @Author : WangJuan # @File : Shell.py """ 封裝執(zhí)行shell語句方法 """ import subprocess class Shell: @staticmethod def invoke(cmd): output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() o = output.decode("utf-8") return o
Page/page.py
class Page: def __init__(self, driver): self.driver = driver @property def initloginpage(self): return Login_Page(self.driver)
Test/test_login.py
登陸的測(cè)試用,我貼一條使用數(shù)據(jù)文件的用例
class Test_login: @pytest.mark.parametrize("args", getData("test_login_error", 'data_error_login')) def test_error_login(self, args): """錯(cuò)誤登陸""" self.page.initloginpage.input_user(args[0]) self.page.initloginpage.input_pwd(args[1]) self.page.initloginpage.click_login() toast_status = self.page.initloginpage.is_toast_exist(args[2]) if toast_status == False: self.page.initpatientpage.take_screenShot() assert False
pytest.ini
pytest配置文件,注釋的是啟動(dòng)失敗重試3次,因?yàn)閍ppium會(huì)因?yàn)橐恍┎豢煽氐脑蚴?,所有正式運(yùn)行腳本的時(shí)候需要加上這個(gè)
[pytest] ;addopts = -s --html=report/report.html --reruns 3 addopts = -s --html=report/report.html testpaths = ./Test python_files = test_*.py python_classes = Test* python_functions = test_add_prescription_list requirements.txt 框架中需要的患教,直接pip install -r requirements.txt 安裝就可以了,可能會(huì)失敗,多試幾次 ```python adbutils==0.3.4 allure-pytest==2.7.0 allure-python-commons==2.7.0 Appium-Python-Client==0.46 atomicwrites==1.3.0 attrs==19.1.0 certifi==2019.6.16 chardet==3.0.4 colorama==0.4.1 coverage==4.5.3 decorator==4.4.0 deprecation==2.0.6 docopt==0.6.2 enum34==1.1.6 facebook-wda==0.3.4 fire==0.1.3 humanize==0.5.1 idna==2.8 importlib-metadata==0.18 logzero==1.5.0 lxml==4.3.4 more-itertools==7.1.0 namedlist==1.7 packaging==19.0 Pillow==6.1.0 pluggy==0.12.0 progress==1.5 py==1.8.0 PyMySQL==0.9.3 pyparsing==2.4.0 pytest==5.0.0 pytest-cov==2.7.1 pytest-html==1.21.1 pytest-metadata==1.8.0 pytest-repeat==0.8.0 pytest-rerunfailures==7.0 PyYAML==5.1.1 requests==2.22.0 retry==0.9.2 selenium==3.141.0 six==1.12.0 tornado==6.0.3 uiautomator2==0.3.3 urllib3==1.25.3 wcwidth==0.1.7 weditor==0.2.3 whichcraft==0.6.0 zipp==0.5.1
到此這篇關(guān)于Appium+Python+pytest自動(dòng)化測(cè)試框架的實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)Appium Python pytest自動(dòng)化測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python list刪除元素時(shí)要注意的坑點(diǎn)分享
下面小編就為大家分享一篇python list刪除元素時(shí)要注意的坑點(diǎn)分享,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-04-04從Python的源碼來解析Python下的freeblock
這篇文章主要介紹了從Python的源碼來解析Python下的freeblock,包括內(nèi)存空間分配等知識(shí),需要的朋友可以參考下2015-05-05Python3實(shí)現(xiàn)捕獲Ctrl+C終止信號(hào)
這篇文章主要為大家詳細(xì)介紹了如何利用Python3實(shí)現(xiàn)捕獲Ctrl+C終止信號(hào)的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03python實(shí)現(xiàn)通過隊(duì)列完成進(jìn)程間的多任務(wù)功能示例
這篇文章主要介紹了python實(shí)現(xiàn)通過隊(duì)列完成進(jìn)程間的多任務(wù)功能,結(jié)合實(shí)例形式分析了Python隊(duì)列完成進(jìn)程間的多任務(wù)以及進(jìn)程池pool相關(guān)操作技巧,需要的朋友可以參考下2019-10-10python數(shù)據(jù)封裝json格式數(shù)據(jù)
本次內(nèi)容是小編在網(wǎng)上整理的關(guān)于如何python數(shù)據(jù)封裝json格式的內(nèi)容總結(jié),有興趣的讀者們參考下。2018-03-03linux下安裝python3和對(duì)應(yīng)的pip環(huán)境教程詳解
這篇文章主要介紹了linux下安裝python3和對(duì)應(yīng)的pip環(huán)境,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07