1 跨瀏覽器測試
1.1 配置selenium standalone Server
下載地址:http://www.seleniumhq.org/download/
1.3 selenium grid2工作原理
Grid 是用於設計幫助我們進行分布式測試的工具,其整個結構有一個hub主節點和若干個node代理節點組成。hub用來管理各個子節點的注冊和狀態信息,並接收遠程客戶端代碼的請求調用,然后把請求的命令再轉發給代理節點來執行。使用Grid遠程執行測試的代碼與直接調用Selenium Server是一樣的,只是環境啟動的方式不一樣,需要同時啟動一個hub和至少一個node。
1.3.1 啟動主節點

以hub形式啟動Server就是一個Gird Server,可以通過瀏覽器查看Grid控制台的信息。

1.3.2 啟動代理節點


1.4 Remote應用
啟動Selenium Server
from selenium.webdriver import Remote import time driver = Remote(command_executor='http://localhost:4444/wd/hub',desired_capabilities= {'platfrom':'ANY','browserName':'firefox','version':'','javascriptEnabled':True}) driver.get('http://baidu.com') driver.find_element_by_id('kw').send_keys('remote') driver.find_element_by_id('su').click() time.sleep(3) driver.quit()
通過Remote()可以參數化使用的瀏覽器。
FireFox = {'platform':'ANY', 'browserName':'firefox', 'version':'', 'javascriptEnabled':True, 'marionette':False }
Chrome = {'platform':'ANY', 'browserName':'chrome', 'version':'', 'javascriptEnabled':True }
Opera= {'platform':'ANY', 'browserName':'opera', 'version':'', 'javascriptEnabled':True }
Iphone= {'platform':'MAC', 'browserName':'iPhone', 'version':'', 'javascriptEnabled':True }
Android = {'platform':'ANDROID', 'browserName':'android', 'version':'', 'javascriptEnabled':True }
1.5 參數化平台及瀏覽器
1.5.1 啟動本地node
先創建list 字典,定義不同的主機ip,端口號及瀏覽器。然后,通過for循環讀取lists字典中的數據作為Remote()的配置信息,從而使腳本在不同的節點及瀏覽器下執行。
from selenium.webdriver import Remote import time lists = {'http://localhost:4444/wd/hub':'chrome','http://localhost:5555/wd/hub':'firefox'} for host,browser in lists.items(): print (host,browser) driver = Remote(command_executor=host,desired_capabilities={'platform': 'ANY','browserName': browser,'version': '','javascriptEnabled': True}) driver.get('http://www.baidu.com') driver.find_element_by_id('kw').send_keys('remote') driver.find_element_by_id('su').click() time.sleep(3) driver.quit()
1.5.2 啟動遠程node
步驟:
from selenium.webdriver import Remote from threading import Thread import time lists = {'http://localhost:4444/wd/hub':'chrome','http://localhost:5555/wd/hub':'firefox'} def WebTest(host,browser): driver = Remote(command_executor=host, desired_capabilities={'platform': 'ANY', 'browserName': browser, 'version': '', 'javascriptEnabled': True}) driver.get('http://www.baidu.com') driver.find_element_by_id('kw').send_keys('remote') driver.find_element_by_id('su').click() time.sleep(3) driver.quit() if __name__ == '__main__': threads=[]
#創建線程 for host, browser in lists.items(): print(host, browser) t = Thread(target=WebTest,args=(host,browser)) threads.append(t)
#啟動線程 for thr in threads: thr.start() print(time.strftime('%Y%m%d%H%M%S'))
2 數據驅動測試與Page Object
使用Python下的數據驅動模式(ddt)庫,結合unittest庫以數據驅動模式創建百度搜索的測試。
pip命令進行下載並安裝:pip install ddt
2.1 一個簡單的數據驅動測試
為了創建數據驅動測試,需要在測試類上使用@ddt裝飾符,在測試方法上使用@data裝飾符。@data裝飾符把參數當作測試數據,參數可以是單個值、列表、元組、字典。對於列表,需要用@unpack裝飾符把元組和列表解析成多個參數。
import unittest,time from selenium import webdriver from ddt import ddt,data,unpack @ddt class WebTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Firefox() cls.driver.implicitly_wait(3) cls.driver.get("http://baidu.com") @classmethod def tearDownClass(cls): cls.driver.quit() @data(("圖書館","圖書館_百度搜索"),("博客","博客_百度搜索")) @unpack # 搜索 def test_search_info(self,search_value, expected_result): self.search = self.driver.find_element_by_xpath("//*[@id='kw']") self.search.clear() self.search.send_keys(search_value) self.search.submit() time.sleep(1.5) self.result = self.driver.title self.assertEqual(expected_result,self.result) if __name__ == '__main__': unittest.main(verbosity=2)
在test_search()方法中,search_value與expected_result兩個參數用來接收元組解析的數據。當運行腳本時,ddt把測試數據轉換為有效的python標識符,生成名稱為更有意義的測試方法。結果如下:

2.2 使用外部數據的數據驅動測試
2.2.1 通過CSV獲取數據
同上在@data裝飾符使用解析外部的CSV(testdata.csv)來作為測試數據(代替之前的測試數據)。其中數據如下:

接下來,先要創建一個get_data()方法,其中包括路徑(這里默認使用當前路徑)、CSV文件名。調用CSV庫去讀取文件並返回一行數據。再使用@ddt及@data實現外部數據驅動測試百度搜索,代碼如下:
import csv,unittest,time from selenium import webdriver from ddt import ddt,data,unpack def GetData(filename): # create an empty list to store rows rows = [] # open the CSV file data_file = open(filename, "r",encoding='utf-8') # create a CSV Reader from CSV file reader = csv.reader(data_file) # skip the headers next(reader, None) # add rows from reader to list for row in reader: rows.append(row) return rows @ddt class WebTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Firefox() cls.driver.implicitly_wait(3) cls.driver.get("http://baidu.com") @classmethod def tearDownClass(cls): cls.driver.quit()
@data(*GetData("testdata.csv")) #@data(*GetData(dir+'\\_test'+'\\testdata.csv')),dir=os.path.dirname(os.path.abspath(__file__)) @unpack # 搜索 def test_search_info(self,search_value, expected_result): self.search = self.driver.find_element_by_xpath("//*[@id='kw']") self.search.clear() self.search.send_keys(search_value) self.search.submit() time.sleep(1.5) self.result = self.driver.title self.assertEqual(expected_result,self.result) if __name__ == '__main__': unittest.main(verbosity=2)
測試執行時,@data將調用get_data()方法讀取外部數據文件,並將數據逐行返回給@data。執行的結果如下:

2.2.2 通過excel獲取數據
讀取excel文件需要用到xlrd的庫,安裝命令:pip install xlrd
創建excel文件如圖:

import xlrd,unittest,time from selenium import webdriver from ddt import ddt,data,unpack import os,sys def GetData(filename): # create an empty list to store rows rows = [] data_file = xlrd.open_workbook(filename,encoding_override='utf-8') sheet = data_file.sheet_by_index(0) #通過索引順序獲取 for row_idx in range(1,sheet.nrows): #從第1行開始獲取 rows.append(list(sheet.row_values(row_idx,0,sheet.ncols))) #從第0列開始獲取 print(rows) return rows @ddt class WebTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Firefox() cls.driver.implicitly_wait(3) cls.driver.get("http://baidu.com") @classmethod def tearDownClass(cls): cls.driver.quit() dir = os.path.dirname(os.path.abspath(__file__)) @data(*GetData("testdata.xlsx")) @unpack # 搜索 def test_search_info(self,search_value, expected_result): self.search = self.driver.find_element_by_xpath("//*[@id='kw']") self.search.clear() self.search.send_keys(search_value) self.search.submit() time.sleep(1.5) self.result = self.driver.title self.assertEqual(expected_result,self.result) if __name__ == '__main__': unittest.main(verbosity=2)
如果想從數據庫的庫表中獲取數據,同樣也需要一個get_data()方法,並且通過DB相關的庫來連接數據庫、SQL查詢來獲取測試數據。
3 Page Object設計模式
Page Object模式,創建一個對象來對應頁面的一個應用。故我們可以為每個頁面定義一個類,並為每個頁面的屬性和操作構建模型。體現在對界面交互細節的封裝,測試在更上層使用頁面對象,在底層的屬性或者操作的更改不會中斷測試。減少代碼重復,提高測試代碼的可讀性和可維護性。
from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep #創建基礎類 class BasePage(object): #初始化 def __init__(self, driver): self.base_url = 'https://mail.qq.com/' self.driver = driver self.timeout = 30 #定義打開登錄頁面方法 def _open(self): url = self.base_url self.driver.get(url) self.driver.switch_to.frame('login_frame') #切換到登錄窗口的iframe #定義定義open方法,調用_open()進行打開 def open(self): self._open() #定位方法封裝 def find_element(self,*loc): return self.driver.find_element(*loc) #創建LoginPage類 class LoginPage(BasePage): username_loc = (By.ID, "u") password_loc = (By.ID, "p") login_loc = (By.ID, "login_button") #輸入用戶名 def type_username(self,username): self.find_element(*self.username_loc).clear() self.find_element(*self.username_loc).send_keys(username) #輸入密碼 def type_password(self,password): self.find_element(*self.password_loc).send_keys(password) #點擊登錄 def type_login(self): self.find_element(*self.login_loc).click() #創建test_user_login()函數 def test_user_login(driver, username, password): """測試用戶名/密碼是否可以登錄""" login_page = LoginPage(driver) login_page.open() login_page.type_username(username) login_page.type_password(password) login_page.type_login() #創建main()函數 def main(): driver = webdriver.Edge() username = '3494xxxxx' #qq號碼 password = 'kemixxxx' #qq密碼 test_user_login(driver, username, password) sleep(3) driver.quit() if __name__ == '__main__': main()
首先創建一個基礎BasePage類,在初始化方法__init__()中定義驅動(driver),基本的URL(base_url)和超時時間(timeout)等。定義open()方法用於打開URL,這里是由_open()方法來實現,而find_element()方法用於元素定位。
接下來的BasePage類中定義的方法都是頁面操作的基本方法。LoginPage類並繼承BasePage類,這也是Page Object設計模式中最重要的對象層。LoginPage類中主要對登錄頁面上元素進行封裝,使其成為具體的操作方法。如對用戶名、密碼框和登錄按鈕都封裝成方法。
然后定義test_user_login()函數將單個元素操作組成一個完整的動作,包含打開瀏覽器、輸入用戶名、密碼並點擊登錄按鈕等。使用時將driver、username、password作為函數的入參,這樣的函數具有很強的可重用性。
最后使用main()函數進行用戶操作行為,現在只關心用哪個瀏覽器、登錄的用戶名和密碼是什么,至少輸入框、按鈕是如何定位的,則不關心。即實現了不同層關心不同問題。如果有多個用戶名/密碼需要登錄,那么只用改寫main()方法的參數即可。
5 自動化測試框架
Test_framework |--config(配置文件) |--data(數據文件) |--drivers(驅動) |--log(日志) |--report(報告) |--test(測試用例) |--utils(公共方法) |--ReadMe.md(加個說明性的文件,告訴團隊成員框架需要的環境以及用法)
把配置抽出來放到config.yml中:
URL: http://www.baidu.com
為了讀取yaml文件,需要一個封裝YamlReader類,在utils中創建file_reader.py文件:
import yaml import os class YamlReader: def __init__(self, yamlf): if os.path.exists(yamlf): self.yamlf = yamlf else: raise FileNotFoundError('文件不存在!') self._data = None @property def data(self): # 如果是第一次調用data,讀取yaml文檔,否則直接返回之前保存的數據 if not self._data: with open(self.yamlf, 'rb') as f: self._data = list(yaml.safe_load_all(f)) # load后是個generator,用list組織成列表 return self._data
需要一個Config類來讀取配置,config.py:
""" 讀取配置。這里配置文件用的yaml,也可用其他如XML,INI等,需在file_reader中添加相應的Reader進行處理。 """ import os from utils.file_reader import YamlReader # 通過當前文件的絕對路徑,其父級目錄一定是框架的base目錄,然后確定各層的絕對路徑。如果你的結構不同,可自行修改。 # 之前直接拼接的路徑,修改了一下,用現在下面這種方法,可以支持linux和windows等不同的平台,使用os.path.split()和os.path.join(),不要直接+'\\xxx\\ss'這樣 BASE_PATH = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] #os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CONFIG_FILE = os.path.join(BASE_PATH, 'config', 'config.yaml') DATA_PATH = os.path.join(BASE_PATH, 'data') DRIVER_PATH = os.path.join(BASE_PATH, 'drivers') LOG_PATH = os.path.join(BASE_PATH, 'log') REPORT_PATH = os.path.join(BASE_PATH, 'report') class Config: def __init__(self, config=CONFIG_FILE): self.config = YamlReader(config).data def get(self, element, index=0): """ yaml是可以通過'---'分節的。用YamlReader讀取返回的是一個list,第一項是默認的節,如果有多個節,可以傳入index來獲取。 把框架相關的配置放在默認節,其他的關於項目的配置放在其他節中。可以在框架中實現多個項目的測試。 """ return self.config[index].get(element)
test.py
import time,os,sys import unittest from selenium import webdriver from selenium.webdriver.common.by import By PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) from utils.config import Config class TestBaiDu(unittest.TestCase): URL = Config().get('URL') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Firefox() self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search_0(self): self.driver.find_element(*self.locator_kw).send_keys('防彈少年團') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) def test_search_1(self): self.driver.find_element(*self.locator_kw).send_keys('復仇者聯盟') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) if __name__ == '__main__': unittest.main()
在utils中創建一個log.py文件,Python有很方便的logging庫,對其進行簡單的封裝,使框架可以很簡單地打印日志(輸出到控制台以及日志文件)。
import os,sys import logging from logging.handlers import TimedRotatingFileHandler PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) from utils.config import LOG_PATH class Logger(object): def __init__(self, logger_name='framework'): self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) self.log_file_name = 'test.log' self.backup_count = 5 # 日志輸出級別 self.console_output_level = 'WARNING' self.file_output_level = 'DEBUG' # 日志輸出格式 self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') def get_logger(self): """在logger中添加日志句柄並返回,如果logger已有句柄,則直接返回""" if not self.logger.handlers: # 避免重復日志 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新創建一個日志文件,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(LOG_PATH, self.log_file_name), when='D', interval=1, backupCount=self.backup_count, delay=True, encoding='utf-8' ) file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger()
test.py
import time,os,sys import unittest from selenium import webdriver from selenium.webdriver.common.by import By PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) from utils.config import Config from utils.log import logger class TestBaiDu(unittest.TestCase): URL = Config().get('URL') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Firefox() self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search_0(self): self.driver.find_element(*self.locator_kw).send_keys('防彈少年團') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) def test_search_1(self): self.driver.find_element(*self.locator_kw).send_keys('復仇者聯盟') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) if __name__ == '__main__': unittest.main()
執行test.py,打印的信息都輸出到log文件夾的test.log文件。

可以把log的設置放到config中,修改config.yml
URL: http://www.baidu.com log: file_name: test.log backup: 5 console_level: WARNING file_level: DEBUG pattern: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
log.py文件
import os,sys import logging from logging.handlers import TimedRotatingFileHandler PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) from utils.config import LOG_PATH,Config class Logger(object): def __init__(self, logger_name='framework'): self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) c = Config().get('log') self.log_file_name = c.get('file_name') if c and c.get('file_name') else 'test.log' # 日志文件 self.backup_count = c.get('backup') if c and c.get('backup') else 5 # 保留的日志數量 # 日志輸出級別 self.console_output_level = c.get('console_level') if c and c.get('console_level') else 'WARNING' self.file_output_level = c.get('file_level') if c and c.get('file_level') else 'DEBUG' # 日志輸出格式 pattern = c.get('pattern') if c and c.get('pattern') else '%(asctime)s - %(name)s - %(levelname)s - %(message)s' self.formatter = logging.Formatter(pattern) def get_logger(self): """在logger中添加日志句柄並返回,如果logger已有句柄,則直接返回""" if not self.logger.handlers: # 避免重復日志 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新創建一個日志文件,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(LOG_PATH, self.log_file_name), when='D', interval=1, backupCount=self.backup_count, delay=True, encoding='utf-8' ) file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger()
修改file_reader.py文件,添加ExcelReader類,實現讀取excel內容的功能:
import yaml import os from xlrd import open_workbook class YamlReader: def __init__(self, yamlf): if os.path.exists(yamlf): self.yamlf = yamlf else: raise FileNotFoundError('文件不存在!') self._data = None @property def data(self): # 如果是第一次調用data,讀取yaml文檔,否則直接返回之前保存的數據 if not self._data: with open(self.yamlf, 'rb') as f: self._data = list(yaml.safe_load_all(f)) # load后是個generator,用list組織成列表 return self._data class SheetTypeError(Exception): pass class ExcelReader: def __init__(self, excel, sheet=0, title_line=True): if os.path.exists(excel): self.excel = excel else: raise FileNotFoundError('文件不存在!') self.sheet = sheet self.title_line = title_line self._data = list() @property def data(self): if not self._data: workbook = open_workbook(self.excel) if type(self.sheet) not in [int, str]: raise SheetTypeError('Please pass in <type int> or <type str>, not {0}'.format(type(self.sheet))) elif type(self.sheet) == int: s = workbook.sheet_by_index(self.sheet) else: s = workbook.sheet_by_name(self.sheet) if self.title_line: title = s.row_values(0) # 首行為title for col in range(1, s.nrows): # 依次遍歷其余行,與首行組成dict,拼到self._data中 self._data.append(dict(zip(title, s.row_values(col)))) else: for col in range(0, s.nrows): # 遍歷所有行,拼到self._data中 self._data.append(s.row_values(col)) return self._data ''' 內部測試 ''' if __name__ == '__main__': y = 'C:\\Users\\zhouxy\\PycharmProjects\\untitled\\TestFramework\\config\\config.yaml' reader = YamlReader(y) print(reader.data) e = 'C:\\Users\\zhouxy\\PycharmProjects\\untitled\\TestFramework\\data\\data.xlsx' reader = ExcelReader(e, title_line=True) print(reader.data)
test.py
import time,os,sys import unittest from selenium import webdriver from selenium.webdriver.common.by import By PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) from utils.config import Config,DATA_PATH from utils.log import logger from utils.file_reader import ExcelReader class TestBaiDu(unittest.TestCase): URL = Config().get('URL') excel = DATA_PATH + '//data.xlsx' locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Firefox() self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search(self): datas = ExcelReader(self.excel).data for d in datas: with self.subTest(data=d): self.setUp() self.driver.find_element(*self.locator_kw).send_keys(d['search']) self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) self.tearDown() if __name__ == '__main__': unittest.main()
test目錄再次進行分層,創建page、common、case、suite四個目錄:
test |--case(用例文件) |--common(跟項目、頁面無關的封裝) |--page(頁面) |--suite(測試套件,用來組織用例)
結合PageObject進行封裝。
