目錄
前言
【文章末尾給大家留下了大量的福利】
測試架構簡介
首先管理時間
添加配置檔案
conf.py
config.ini
讀取配置檔案
記錄記錄檔
簡單了解POM模型
簡單學習元素定位
管理頁面元素
封裝Selenium基類
建立頁面對象
簡單了解Pytest
pytest.ini
編寫測試用例
conftest.py
執行用例
發送郵件
pytest使用allure測試報告
allure安裝
allure初體驗
allure裝飾器介紹
報告的生成和展示
allure發生錯誤截圖
開源位址
前言
selenium自動化+ pytest測試架構+allure報告
本章你需要
- 一定的python基礎——至少明白類與對象,封裝繼承
- 一定的selenium基礎——本篇不講selenium,不會的可以自己去看selenium中文翻譯網
-
【文章末尾給大家留下了大量的福利】
測試架構簡介
- 測試架構有什麼優點呢:
- 代碼複用率高,如果不使用架構的話,代碼會很備援
- 可以組裝日志、報告、郵件等一些進階功能
- 提高元素等資料的可維護性,元素發生變化時,隻需要更新一下配置檔案
- 使用更靈活的PageObject設計模式
- 測試架構的整體目錄
目錄/檔案 說明 是否為python包 common 這個包中存放的是常見的通用的類,如讀取配置檔案 是 config 配置檔案目錄 是 logs 日志目錄 page 對selenium的方放進行深度的封裝 是 page_element 頁面元素存放目錄 page_object 頁面對象POM設計模式,本人對這個的了解來自于苦葉子的部落格 是 TestCase 所有的測試用例集 是 utils 工具類 是 script 腳本檔案 conftest.py pytest膠水檔案 pytest.ini pytest配置檔案
這樣一個簡單的架構結構就清晰了。
知道了以上這些我們就開始吧!
我們在項目中先按照上面的架構指引,建好每一項目錄。
注意:python包為是的,都需要添加一個
__init__.py
檔案以辨別此目錄為一個python包。
首先管理時間
首先呢,因為我們很多的子產品會用到時間戳,或者日期等等字元串,是以我們先單獨把時間封裝成一個子產品。
然後讓其他子產品來調用即可。在
utils
目錄建立
times.py
子產品
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> time
<span style="color:#7171bf">import</span> datetime
<span style="color:#7171bf">from</span> functools <span style="color:#7171bf">import</span> wraps
<span style="color:#7171bf">def</span> <span style="color:#61aeee">timestamp</span>():
<span style="color:#98c379">"""時間戳"""</span>
<span style="color:#7171bf">return</span> time.time()
<span style="color:#7171bf">def</span> <span style="color:#61aeee">dt_strftime</span>(fmt=<span style="color:#98c379">"%Y%m"</span>):
<span style="color:#98c379">"""
datetime格式化時間
:param fmt "%Y%m%d %H%M%S
"""</span>
<span style="color:#7171bf">return</span> datetime.datetime.now().strftime(fmt)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">sleep</span>(seconds=<span style="color:#d19a66">1.0</span>):
<span style="color:#98c379">"""
睡眠時間
"""</span>
time.sleep(seconds)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">running_time</span>(func):
<span style="color:#98c379">"""函數運作時間"""</span>
<span style="color:#61aeee"> @wraps(func)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">wrapper</span>(*args, **kwargs):
start = timestamp()
res = func(*args, **kwargs)
<span style="color:#7171bf">print</span>(<span style="color:#98c379">"校驗元素done!用時%.3f秒!"</span> % (timestamp() - start))
<span style="color:#7171bf">return</span> res
<span style="color:#7171bf">return</span> wrapper
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
<span style="color:#7171bf">print</span>(dt_strftime(<span style="color:#98c379">"%Y%m%d%H%M%S"</span>))
</code></span></span>
添加配置檔案
配置檔案總是項目中必不可少的部分!
将固定不變的資訊集中在固定的檔案中
conf.py
項目中都應該有一個檔案對整體的目錄進行管理,我也在這個python項目中設定了此檔案。
在項目
config
目錄建立
conf.py
檔案,所有的目錄配置資訊寫在這個檔案裡面。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">from</span> selenium.webdriver.common.by <span style="color:#7171bf">import</span> By
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> dt_strftime
<span style="color:#7171bf">class</span> <span style="color:#61aeee">ConfigManager</span>(<span style="color:#61aeee">object</span>):
<span style="color:#5c6370"><em># 項目目錄</em></span>
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
<span style="color:#5c6370"><em># 頁面元素目錄</em></span>
ELEMENT_PATH = os.path.join(BASE_DIR, <span style="color:#98c379">'page_element'</span>)
<span style="color:#5c6370"><em># 報告檔案</em></span>
REPORT_FILE = os.path.join(BASE_DIR, <span style="color:#98c379">'report.html'</span>)
<span style="color:#5c6370"><em># 元素定位的類型</em></span>
LOCATE_MODE = {
<span style="color:#98c379">'css'</span>: By.CSS_SELECTOR,
<span style="color:#98c379">'xpath'</span>: By.XPATH,
<span style="color:#98c379">'name'</span>: By.NAME,
<span style="color:#98c379">'id'</span>: By.ID,
<span style="color:#98c379">'class'</span>: By.CLASS_NAME
}
<span style="color:#5c6370"><em># 郵件資訊</em></span>
EMAIL_INFO = {
<span style="color:#98c379">'username'</span>: <span style="color:#98c379">'[email protected]'</span>, <span style="color:#5c6370"><em># 切換成你自己的位址</em></span>
<span style="color:#98c379">'password'</span>: <span style="color:#98c379">'QQ郵箱授權碼'</span>,
<span style="color:#98c379">'smtp_host'</span>: <span style="color:#98c379">'smtp.qq.com'</span>,
<span style="color:#98c379">'smtp_port'</span>: <span style="color:#d19a66">465</span>
}
<span style="color:#5c6370"><em># 收件人</em></span>
ADDRESSEE = [
<span style="color:#98c379">'[email protected]'</span>,
]
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">log_file</span>(self):
<span style="color:#98c379">"""日志目錄"""</span>
log_dir = os.path.join(self.BASE_DIR, <span style="color:#98c379">'logs'</span>)
<span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(log_dir):
os.makedirs(log_dir)
<span style="color:#7171bf">return</span> os.path.join(log_dir, <span style="color:#98c379">'{}.log'</span>.<span style="color:#7171bf">format</span>(dt_strftime()))
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">ini_file</span>(self):
<span style="color:#98c379">"""配置檔案"""</span>
ini_file = os.path.join(self.BASE_DIR, <span style="color:#98c379">'config'</span>, <span style="color:#98c379">'config.ini'</span>)
<span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(ini_file):
<span style="color:#7171bf">raise</span> FileNotFoundError(<span style="color:#98c379">"配置檔案%s不存在!"</span> % ini_file)
<span style="color:#7171bf">return</span> ini_file
cm = ConfigManager()
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
<span style="color:#7171bf">print</span>(cm.BASE_DIR)
</code></span></span>
注意:QQ郵箱授權碼:點選檢視生成教程
這個conf檔案我模仿了Django的settings.py檔案的設定風格,但是又有些許差異。
在這個檔案中我們可以設定自己的各個目錄,也可以檢視自己目前的目錄。
遵循了約定:不變的常量名全部大寫,函數名小寫。看起來整體美觀。
config.ini
在項目
config
目錄建立一個
config.ini
檔案,裡面暫時先放入我們的需要測試的URL
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-ini"><span style="color:#e06c75">[HOST]</span>
<span style="color:#d19a66">HOST</span> = https://www.baidu.com
</code></span></span>
讀取配置檔案
配置檔案建立好了,接下來我們需要讀取這個配置檔案以使用裡面的資訊。
我們在
common
目錄中建立一個
readconfig.py
檔案
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> configparser
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
HOST = <span style="color:#98c379">'HOST'</span>
<span style="color:#7171bf">class</span> <span style="color:#61aeee">ReadConfig</span>(<span style="color:#61aeee">object</span>):
<span style="color:#98c379">"""配置檔案"""</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self):
self.config = configparser.RawConfigParser() <span style="color:#5c6370"><em># 當有%的符号時請使用Raw讀取</em></span>
self.config.read(cm.ini_file, encoding=<span style="color:#98c379">'utf-8'</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">_get</span>(self, section, option):
<span style="color:#98c379">"""擷取"""</span>
<span style="color:#7171bf">return</span> self.config.get(section, option)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">_set</span>(self, section, option, value):
<span style="color:#98c379">"""更新"""</span>
self.config.<span style="color:#7171bf">set</span>(section, option, value)
<span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(cm.ini_file, <span style="color:#98c379">'w'</span>) <span style="color:#7171bf">as</span> f:
self.config.write(f)
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">url</span>(self):
<span style="color:#7171bf">return</span> self._get(HOST, HOST)
ini = ReadConfig()
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
<span style="color:#7171bf">print</span>(ini.url)
</code></span></span>
可以看到我們用python内置的configparser子產品對
config.ini
檔案進行了讀取。
對于url值的提取,我使用了高階文法
@property
屬性值,寫法更簡單。
記錄記錄檔
日志,大家應該都很熟悉這個名詞,就是記錄代碼中的動作。
在
utils
目錄中建立
logger.py
檔案。
這個檔案就是我們用來在自動化測試過程中記錄一些操作步驟的。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> logging
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">class</span> <span style="color:#61aeee">Log</span>:
<span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self):
self.logger = logging.getLogger()
<span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> self.logger.handlers:
self.logger.setLevel(logging.DEBUG)
<span style="color:#5c6370"><em># 建立一個handle寫入檔案</em></span>
fh = logging.FileHandler(cm.log_file, encoding=<span style="color:#98c379">'utf-8'</span>)
fh.setLevel(logging.INFO)
<span style="color:#5c6370"><em># 建立一個handle輸出到控制台</em></span>
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
<span style="color:#5c6370"><em># 定義輸出的格式</em></span>
formatter = logging.Formatter(self.fmt)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
<span style="color:#5c6370"><em># 添加到handle</em></span>
self.logger.addHandler(fh)
self.logger.addHandler(ch)
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">fmt</span>(self):
<span style="color:#7171bf">return</span> <span style="color:#98c379">'%(levelname)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s'</span>
log = Log().logger
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
log.info(<span style="color:#98c379">'hello world'</span>)
</code></span></span>
在終端中運作該檔案,就看到指令行列印出了:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">INFO 2020-12-01 16:00:05,467 [logger.py:38] hello world
</code></span></span>
然後在項目logs目錄下生成了當月的日志檔案。
簡單了解POM模型
由于下面要講元素相關的,是以首先了解一下POM模型
Page Object模式具有以下幾個優點。
該觀點來自 《Selenium自動化測試——基于Python語言》
-
抽象出對象可以最大程度地降低開發人員修改頁面代碼對測試的影響, 是以, 你僅需要對頁
面對象進行調整, 而對測試沒有影響;
- 可以在多個測試用例中複用一部分測試代碼;
- 測試代碼變得更易讀、 靈活、 可維護
Page Object模式圖
- basepage ——selenium的基類,對selenium的方法進行封裝
- pageelements——頁面元素,把頁面元素單獨提取出來,放入一個檔案中
- searchpage ——頁面對象類,把selenium方法和頁面元素進行整合
- testcase ——使用pytest對整合的searchpage進行測試用例編寫
通過上圖我們可以看出,通過POM模型思想,我們把:
- selenium方法
- 頁面元素
- 頁面對象
- 測試用例
以上四種代碼主體進行了拆分,雖然在用例很少的情況下做會增加代碼,但是當用例多的時候意義很大,代碼量會在用例增加的時候顯著減少。我們維護代碼變得更加直覺明顯,代碼可讀性也變得比工廠模式強很多,代碼複用率也極大的得到了提高。
簡單學習元素定位
在日常的工作中,我見過很多在浏覽器中直接在浏覽器中右鍵
Copy Xpath
複制元素的同學。這樣獲得的元素表達式放在 webdriver 中去運作往往是不夠穩定的,像前端的一些微小改動,都會引起元素無法定位的
NoSuchElementException
報錯。
是以在實際工作和學習中我們應該加強自己的元素定位能力,盡可能的采用
xpath
和
CSS selector
這種相對穩定的定位文法。由于
CSS selector
的文法生硬難懂,對新手很不友好,而且相比
xpath
缺少一些定位文法。是以我們選擇
xpath
進行我們的元素定位文法。
xpath#
文法規則
菜鳥教程中對于 xpath 的介紹是一門在 XML 文檔中查找資訊的語言。
表達式 | 介紹 | 備注 |
---|---|---|
/ | 根節點 | 絕對路徑 |
// | 目前節點的所有子節點 | 相對路徑 |
* | 所有節點元素的 | |
@ | 屬性名的字首 | @class @id |
*[1] | [] 下标運算符 | |
[] | [ ]謂詞表達式 | //input[@id='kw'] |
Following-sibling | 目前節點之後的同級 | |
preceding-sibling | 目前節點之前的同級 | |
parent | 目前節點的父級節點 |
定位工具
- chropath
- 優點:這是一個Chrome浏覽器的測試定位插件,類似于firepath,本人試用了一下整體感覺非常好。對小白的友好度很好。
- 缺點:安裝這個插件需要FQ。
- Katalon錄制工具
- 錄制出來的腳本裡面也會有定位元素的資訊
- 自己寫——本人推薦這種
- 優點:本人推薦的方式,因為當熟練到一定程度的時候,寫出來的會更直覺簡潔,并且在運作自動化測試中出現問題時,能快速定位。
- 缺點:需要一定
和xpath
文法積累,不太容易上手。CSS selector
管理頁面元素
本教程選擇的測試位址是百度首頁,是以對應的元素也是百度首頁的。
項目架構設計中有一個目錄
page_element
就是專門來存放定位元素的檔案的。
通過對各種配置檔案的對比,我在這裡選擇的是YAML檔案格式。其易讀,互動性好。
我們在
page_element
中建立一個
search.yaml
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-yaml"><span style="color:#98c379">搜尋框:</span> <span style="color:#98c379">"id==kw"</span>
<span style="color:#98c379">候選:</span> <span style="color:#98c379">"css==.bdsug-overflow"</span>
<span style="color:#98c379">搜尋候選:</span> <span style="color:#98c379">"css==#form div li"</span>
<span style="color:#98c379">搜尋按鈕:</span> <span style="color:#98c379">"id==su"</span>
</code></span></span>
元素定位檔案建立好了,下來我們需要讀取這個檔案。
在
common
目錄中建立
readelement.py
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">import</span> yaml
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">class</span> <span style="color:#61aeee">Element</span>(<span style="color:#61aeee">object</span>):
<span style="color:#98c379">"""擷取元素"""</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self, name):
self.file_name = <span style="color:#98c379">'%s.yaml'</span> % name
self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
<span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(self.element_path):
<span style="color:#7171bf">raise</span> FileNotFoundError(<span style="color:#98c379">"%s 檔案不存在!"</span> % self.element_path)
<span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(self.element_path, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
self.data = yaml.safe_load(f)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">__getitem__</span>(self, item):
<span style="color:#98c379">"""擷取屬性"""</span>
data = self.data.get(item)
<span style="color:#7171bf">if</span> data:
name, value = data.split(<span style="color:#98c379">'=='</span>)
<span style="color:#7171bf">return</span> name, value
<span style="color:#7171bf">raise</span> ArithmeticError(<span style="color:#98c379">"{}中不存在關鍵字:{}"</span>.<span style="color:#7171bf">format</span>(self.file_name, item))
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
search = Element(<span style="color:#98c379">'search'</span>)
<span style="color:#7171bf">print</span>(search[<span style="color:#98c379">'搜尋框'</span>])
</code></span></span>
通過特殊方法
__getitem__
實作調用任意屬性,讀取yaml中的值。
這樣我們就實作了定位元素的存儲和調用。
但是還有一個問題,我們怎麼樣才能確定我們寫的每一項元素不出錯,人為的錯誤是不可避免的,但是我們可以通過代碼來運作對檔案的審查。目前也不能所有問題都能發現。
是以我們編寫一個檔案,在
script
腳本檔案目錄中建立
inspect.py
檔案,對所有的元素yaml檔案進行審查。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> os
<span style="color:#7171bf">import</span> yaml
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> running_time
<span style="color:#61aeee">@running_time</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">inspect_element</span>():
<span style="color:#98c379">"""檢查所有的元素是否正确
隻能做一個簡單的檢查
"""</span>
<span style="color:#7171bf">for</span> files <span style="color:#7171bf">in</span> os.listdir(cm.ELEMENT_PATH):
_path = os.path.join(cm.ELEMENT_PATH, files)
<span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(_path, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
data = yaml.safe_load(f)
<span style="color:#7171bf">for</span> k <span style="color:#7171bf">in</span> data.values():
<span style="color:#7171bf">try</span>:
pattern, value = k.split(<span style="color:#98c379">'=='</span>)
<span style="color:#7171bf">except</span> ValueError:
<span style="color:#7171bf">raise</span> Exception(<span style="color:#98c379">"元素表達式中沒有`==`"</span>)
<span style="color:#7171bf">if</span> pattern <span style="color:#7171bf">not</span> <span style="color:#7171bf">in</span> cm.LOCATE_MODE:
<span style="color:#7171bf">raise</span> Exception(<span style="color:#98c379">'%s中元素【%s】沒有指定類型'</span> % (_path, k))
<span style="color:#7171bf">elif</span> pattern == <span style="color:#98c379">'xpath'</span>:
<span style="color:#7171bf">assert</span> <span style="color:#98c379">'//'</span> <span style="color:#7171bf">in</span> value,\
<span style="color:#98c379">'%s中元素【%s】xpath類型與值不配'</span> % (_path, k)
<span style="color:#7171bf">elif</span> pattern == <span style="color:#98c379">'css'</span>:
<span style="color:#7171bf">assert</span> <span style="color:#98c379">'//'</span> <span style="color:#7171bf">not</span> <span style="color:#7171bf">in</span> value, \
<span style="color:#98c379">'%s中元素【%s]css類型與值不配'</span> % (_path, k)
<span style="color:#7171bf">else</span>:
<span style="color:#7171bf">assert</span> value, <span style="color:#98c379">'%s中元素【%s】類型與值不比對'</span> % (_path, k)
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
inspect_element()
</code></span></span>
執行該檔案:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">校驗元素done!用時<span style="color:#d19a66">0.002</span>秒!
</code></span></span>
可以看到,很短的時間内,我們就對所填寫的YAML檔案進行了審查。
現在我們基本所需要的元件已經大緻完成了。
接下來我們将進行最重要的一環,封裝selenium。
封裝Selenium基類
在工廠模式種我們是這樣寫的:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> time
<span style="color:#7171bf">from</span> selenium <span style="color:#7171bf">import</span> webdriver
driver = webdriver.Chrome()
driver.get(<span style="color:#98c379">'https://www.baidu.com'</span>)
driver.find_element_by_xpath(<span style="color:#98c379">"//input[@id='kw']"</span>).send_keys(<span style="color:#98c379">'selenium'</span>)
driver.find_element_by_xpath(<span style="color:#98c379">"//input[@id='su']"</span>).click()
time.sleep(<span style="color:#d19a66">5</span>)
driver.quit()
</code></span></span>
很直白,簡單,又明了。
建立driver對象,打開百度網頁,搜尋selenium,點選搜尋,然後停留5秒,檢視結果,最後關閉浏覽器。
那我們為什麼要封裝selenium的方法呢。首先我們上述這種較為原始的方法,基本不适用于平時做UI自動化測試的,因為在UI界面實際運作情況遠遠比較複雜,可能因為網絡原因,或者控件原因,我們元素還沒有顯示出來,就進行點選或者輸入。是以我們需要封裝selenium方法,通過内置的顯式等待或一定的條件語句,才能建構一個穩定的方法。而且把selenium方法封裝起來,有利于平時的代碼維護。
我們在
page
目錄建立
webpage.py
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#98c379">"""
selenium基類
本檔案存放了selenium基類的封裝方法
"""</span>
<span style="color:#7171bf">from</span> selenium.webdriver.support <span style="color:#7171bf">import</span> expected_conditions <span style="color:#7171bf">as</span> EC
<span style="color:#7171bf">from</span> selenium.webdriver.support.ui <span style="color:#7171bf">import</span> WebDriverWait
<span style="color:#7171bf">from</span> selenium.common.exceptions <span style="color:#7171bf">import</span> TimeoutException
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> sleep
<span style="color:#7171bf">from</span> utils.logger <span style="color:#7171bf">import</span> log
<span style="color:#7171bf">class</span> <span style="color:#61aeee">WebPage</span>(<span style="color:#61aeee">object</span>):
<span style="color:#98c379">"""selenium基類"""</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">__init__</span>(self, driver):
<span style="color:#5c6370"><em># self.driver = webdriver.Chrome()</em></span>
self.driver = driver
self.timeout = <span style="color:#d19a66">20</span>
self.wait = WebDriverWait(self.driver, self.timeout)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">get_url</span>(self, url):
<span style="color:#98c379">"""打開網址并驗證"""</span>
self.driver.maximize_window()
self.driver.set_page_load_timeout(<span style="color:#d19a66">60</span>)
<span style="color:#7171bf">try</span>:
self.driver.get(url)
self.driver.implicitly_wait(<span style="color:#d19a66">10</span>)
log.info(<span style="color:#98c379">"打開網頁:%s"</span> % url)
<span style="color:#7171bf">except</span> TimeoutException:
<span style="color:#7171bf">raise</span> TimeoutException(<span style="color:#98c379">"打開%s逾時請檢查網絡或網址伺服器"</span> % url)
<span style="color:#61aeee"> @staticmethod</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">element_locator</span>(func, locator):
<span style="color:#98c379">"""元素定位器"""</span>
name, value = locator
<span style="color:#7171bf">return</span> func(cm.LOCATE_MODE[name], value)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">find_element</span>(self, locator):
<span style="color:#98c379">"""尋找單個元素"""</span>
<span style="color:#7171bf">return</span> WebPage.element_locator(<span style="color:#7171bf">lambda</span> *args: self.wait.until(
EC.presence_of_element_located(args)), locator)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">find_elements</span>(self, locator):
<span style="color:#98c379">"""查找多個相同的元素"""</span>
<span style="color:#7171bf">return</span> WebPage.element_locator(<span style="color:#7171bf">lambda</span> *args: self.wait.until(
EC.presence_of_all_elements_located(args)), locator)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">elements_num</span>(self, locator):
<span style="color:#98c379">"""擷取相同元素的個數"""</span>
number = <span style="color:#7171bf">len</span>(self.find_elements(locator))
log.info(<span style="color:#98c379">"相同元素:{}"</span>.<span style="color:#7171bf">format</span>((locator, number)))
<span style="color:#7171bf">return</span> number
<span style="color:#7171bf">def</span> <span style="color:#61aeee">input_text</span>(self, locator, txt):
<span style="color:#98c379">"""輸入(輸入前先清空)"""</span>
sleep(<span style="color:#d19a66">0.5</span>)
ele = self.find_element(locator)
ele.clear()
ele.send_keys(txt)
log.info(<span style="color:#98c379">"輸入文本:{}"</span>.<span style="color:#7171bf">format</span>(txt))
<span style="color:#7171bf">def</span> <span style="color:#61aeee">is_click</span>(self, locator):
<span style="color:#98c379">"""點選"""</span>
self.find_element(locator).click()
sleep()
log.info(<span style="color:#98c379">"點選元素:{}"</span>.<span style="color:#7171bf">format</span>(locator))
<span style="color:#7171bf">def</span> <span style="color:#61aeee">element_text</span>(self, locator):
<span style="color:#98c379">"""擷取目前的text"""</span>
_text = self.find_element(locator).text
log.info(<span style="color:#98c379">"擷取文本:{}"</span>.<span style="color:#7171bf">format</span>(_text))
<span style="color:#7171bf">return</span> _text
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">get_source</span>(self):
<span style="color:#98c379">"""擷取頁面源代碼"""</span>
<span style="color:#7171bf">return</span> self.driver.page_source
<span style="color:#7171bf">def</span> <span style="color:#61aeee">refresh</span>(self):
<span style="color:#98c379">"""重新整理頁面F5"""</span>
self.driver.refresh()
self.driver.implicitly_wait(<span style="color:#d19a66">30</span>)
</code></span></span>
在檔案中我們對主要用了
顯式等待
對selenium的click,send_keys等方法,做了二次封裝。提高了運作的成功率。
好了我們完成了POM模型的一半左右的内容。接下來我們們進入頁面對象。
建立頁面對象
在
page_object
目錄下建立一個
searchpage.py
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">from</span> page.webpage <span style="color:#7171bf">import</span> WebPage, sleep
<span style="color:#7171bf">from</span> common.readelement <span style="color:#7171bf">import</span> Element
search = Element(<span style="color:#98c379">'search'</span>)
<span style="color:#7171bf">class</span> <span style="color:#61aeee">SearchPage</span>(<span style="color:#61aeee">WebPage</span>):
<span style="color:#98c379">"""搜尋類"""</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">input_search</span>(self, content):
<span style="color:#98c379">"""輸入搜尋"""</span>
self.input_text(search[<span style="color:#98c379">'搜尋框'</span>], txt=content)
sleep()
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">imagine</span>(self):
<span style="color:#98c379">"""搜尋聯想"""</span>
<span style="color:#7171bf">return</span> [x.text <span style="color:#7171bf">for</span> x <span style="color:#7171bf">in</span> self.find_elements(search[<span style="color:#98c379">'候選'</span>])]
<span style="color:#7171bf">def</span> <span style="color:#61aeee">click_search</span>(self):
<span style="color:#98c379">"""點選搜尋"""</span>
self.is_click(search[<span style="color:#98c379">'搜尋按鈕'</span>])
</code></span></span>
在該檔案中我們對,輸入搜尋關鍵詞,點選搜尋,搜尋聯想,進行了封裝。
并配置了注釋。
在平時中我們應該養成寫注釋的習慣,因為過一段時間後,沒有注釋,代碼讀起來很費勁。
好了我們的頁面對象此時業已完成了。下面我們開始編寫測試用例。在開始測試用了之前我們先熟悉一下pytest測試架構。
簡單了解Pytest
打開pytest架構的官網。pytest: helps you write better programs — pytest documentation
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em># content of test_sample.py</em></span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">inc</span>(x):
<span style="color:#7171bf">return</span> x + <span style="color:#d19a66">1</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_answer</span>():
<span style="color:#7171bf">assert</span> inc(<span style="color:#d19a66">3</span>) == <span style="color:#d19a66">5</span>
</code></span></span>
pytest.ini
pytest項目中的配置檔案,可以對pytest執行過程中操作做全局控制。
在項目根目錄建立
pytest.ini
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-ini"><span style="color:#e06c75">[pytest]</span>
<span style="color:#d19a66">addopts</span> = --html=report.html --self-contained-html
</code></span></span>
- addopts 指定執行時的其他參數說明:
-
生成pytest-html帶樣式的報告--html=report/report.html --self-contained-html
-
輸出我們用例中的調式資訊-s
-
安靜的進行測試-q
-
可以輸出用例更加詳細的執行資訊,比如用例所在的檔案及用例名稱等-v
-
編寫測試用例
我們将使用pytest編寫測試用例。
在
TestCase
目錄中建立
test_search.py
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> re
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">from</span> utils.logger <span style="color:#7171bf">import</span> log
<span style="color:#7171bf">from</span> common.readconfig <span style="color:#7171bf">import</span> ini
<span style="color:#7171bf">from</span> page_object.searchpage <span style="color:#7171bf">import</span> SearchPage
<span style="color:#7171bf">class</span> <span style="color:#61aeee">TestSearch</span>:
<span style="color:#61aeee"> @pytest.fixture(scope=<span style="color:#3388aa">'function'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">open_baidu</span>(self, drivers):
<span style="color:#98c379">"""打開百度"""</span>
search = SearchPage(drivers)
search.get_url(ini.url)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_001</span>(self, drivers):
<span style="color:#98c379">"""搜尋"""</span>
search = SearchPage(drivers)
search.input_search(<span style="color:#98c379">"selenium"</span>)
search.click_search()
result = re.search(<span style="color:#98c379">r'selenium'</span>, search.get_source)
log.info(result)
<span style="color:#7171bf">assert</span> result
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_002</span>(self, drivers):
<span style="color:#98c379">"""測試搜尋候選"""</span>
search = SearchPage(drivers)
search.input_search(<span style="color:#98c379">"selenium"</span>)
log.info(<span style="color:#7171bf">list</span>(search.imagine))
<span style="color:#7171bf">assert</span> <span style="color:#7171bf">all</span>([<span style="color:#98c379">"selenium"</span> <span style="color:#7171bf">in</span> i <span style="color:#7171bf">for</span> i <span style="color:#7171bf">in</span> search.imagine])
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
pytest.main([<span style="color:#98c379">'TestCase/test_search.py'</span>])
</code></span></span>
我們測試用了就編寫好了。
- pytest.fixture 這個實作了和unittest的setup,teardown一樣的前置啟動,後置清理的裝飾器。
- 第一個測試用例:
- 我們實作了在百度selenium關鍵字,并點選搜尋按鈕,并在搜尋結果中,用正則查找結果頁源代碼,傳回數量大于10我們就認為通過。
- 第二個測試用例:
- 我們實作了,搜尋selenium,然後斷言搜尋候選中的所有結果有沒有selenium關鍵字。
最後我們的在下面寫一個執行啟動的語句。
這時候我們應該進入執行了,但是還有一個問題,我們還沒有把driver傳遞。
conftest.py
我們在項目根目錄下建立一個
conftest.py
檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">from</span> py.xml <span style="color:#7171bf">import</span> html
<span style="color:#7171bf">from</span> selenium <span style="color:#7171bf">import</span> webdriver
driver = <span style="color:#56b6c2">None</span>
<span style="color:#61aeee">@pytest.fixture(scope=<span style="color:#3388aa">'session'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">drivers</span>(request):
<span style="color:#7171bf">global</span> driver
<span style="color:#7171bf">if</span> driver <span style="color:#7171bf">is</span> <span style="color:#56b6c2">None</span>:
driver = webdriver.Chrome()
driver.maximize_window()
<span style="color:#7171bf">def</span> <span style="color:#61aeee">fn</span>():
driver.quit()
request.addfinalizer(fn)
<span style="color:#7171bf">return</span> driver
<span style="color:#61aeee">@pytest.hookimpl(hookwrapper=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_runtest_makereport</span>(item):
<span style="color:#98c379">"""
當測試失敗的時候,自動截圖,展示到html報告中
:param item:
"""</span>
pytest_html = item.config.pluginmanager.getplugin(<span style="color:#98c379">'html'</span>)
outcome = <span style="color:#7171bf">yield</span>
report = outcome.get_result()
report.description = <span style="color:#7171bf">str</span>(item.function.__doc__)
extra = <span style="color:#7171bf">getattr</span>(report, <span style="color:#98c379">'extra'</span>, [])
<span style="color:#7171bf">if</span> report.when == <span style="color:#98c379">'call'</span> <span style="color:#7171bf">or</span> report.when == <span style="color:#98c379">"setup"</span>:
xfail = <span style="color:#7171bf">hasattr</span>(report, <span style="color:#98c379">'wasxfail'</span>)
<span style="color:#7171bf">if</span> (report.skipped <span style="color:#7171bf">and</span> xfail) <span style="color:#7171bf">or</span> (report.failed <span style="color:#7171bf">and</span> <span style="color:#7171bf">not</span> xfail):
file_name = report.nodeid.replace(<span style="color:#98c379">"::"</span>, <span style="color:#98c379">"_"</span>) + <span style="color:#98c379">".png"</span>
screen_img = _capture_screenshot()
<span style="color:#7171bf">if</span> file_name:
html = <span style="color:#98c379">'<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:1024px;height:768px;" '</span> \
<span style="color:#98c379">'onclick="window.open(this.src)" align="right"/></div>'</span> % screen_img
extra.append(pytest_html.extras.html(html))
report.extra = extra
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_header</span>(cells):
cells.insert(<span style="color:#d19a66">1</span>, html.th(<span style="color:#98c379">'用例名稱'</span>))
cells.insert(<span style="color:#d19a66">2</span>, html.th(<span style="color:#98c379">'Test_nodeid'</span>))
cells.pop(<span style="color:#d19a66">2</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_row</span>(report, cells):
cells.insert(<span style="color:#d19a66">1</span>, html.td(report.description))
cells.insert(<span style="color:#d19a66">2</span>, html.td(report.nodeid))
cells.pop(<span style="color:#d19a66">2</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_html</span>(report, data):
<span style="color:#7171bf">if</span> report.passed:
<span style="color:#7171bf">del</span> data[:]
data.append(html.div(<span style="color:#98c379">'通過的用例未捕獲日志輸出.'</span>, class_=<span style="color:#98c379">'empty log'</span>))
<span style="color:#7171bf">def</span> <span style="color:#61aeee">_capture_screenshot</span>():
<span style="color:#98c379">'''
截圖儲存為base64
:return:
'''</span>
<span style="color:#7171bf">return</span> driver.get_screenshot_as_base64()
</code></span></span>
conftest.py測試架構pytest的膠水檔案,裡面用到了fixture的方法,封裝并傳遞出了driver。
執行用例
以上我們已經編寫完成了整個架構和測試用例。
我們進入到目前項目的主目錄執行指令:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">pytest
</code></span></span>
指令行輸出:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">Test session starts (platform: win32, Python <span style="color:#d19a66">3.7</span>.<span style="color:#d19a66">7</span>, pytest <span style="color:#d19a66">5.3</span>.<span style="color:#d19a66">2</span>, py<span style="color:#7171bf">test-sugar</span> <span style="color:#d19a66">0.9</span>.<span style="color:#d19a66">2</span>)
cachedir: .pytest_cache
metadata: {<span style="color:#98c379">'Python'</span>: <span style="color:#98c379">'3.7.7'</span>, <span style="color:#98c379">'Platform'</span>: <span style="color:#98c379">'Windows-10-10.0.18362-SP0'</span>, <span style="color:#98c379">'Packages'</span>: {<span style="color:#98c379">'pytest'</span>: <span style="color:#98c379">'5.3.2'</span>, <span style="color:#98c379">'py'</span>: <span style="color:#98c379">'1.8.0'</span>, <span style="color:#98c379">'pluggy'</span>: <span style="color:#98c379">'0.13.1'</span>}, <span style="color:#98c379">'Plugins'</span>: {<span style="color:#98c379">'forked'</span>: <span style="color:#98c379">'1.1.3'</span>, <span style="color:#98c379">'html'</span>: <span style="color:#98c379">'2.0.1'</span>, <span style="color:#98c379">'metadata'</span>: <span style="color:#98c379">'1.8.0'</span>, <span style="color:#98c379">'ordering'</span>: <span style="color:#98c379">'0.6'</span>, <span style="color:#98c379">'rerunfailures'</span>: <span style="color:#98c379">'8.0'</span>, <span style="color:#98c379">'sugar'</span>: <span style="color:#98c379">'0.9.2'</span>, <span style="color:#98c379">'xdist'</span>: <span style="color:#98c379">'1.31.0'</span>}, <span style="color:#98c379">'JAVA_HOME'</span>: <span style="color:#98c379">'D:\\Program Files\\Java\\jdk1.8.0_131'</span>}
rootdir: C:\Users\hoou\PycharmProjects\web<span style="color:#56b6c2">-demotest</span>, inifile: pytest.ini
plugins: forked<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">1.3</span>, html<span style="color:#56b6c2">-2</span>.<span style="color:#d19a66">0.1</span>, metadata<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">8.0</span>, ordering<span style="color:#56b6c2">-0</span>.<span style="color:#d19a66">6</span>, rerunfailures<span style="color:#56b6c2">-8</span>.<span style="color:#d19a66">0</span>, sugar<span style="color:#56b6c2">-0</span>.<span style="color:#d19a66">9.2</span>, xdist<span style="color:#56b6c2">-1</span>.<span style="color:#d19a66">31.0</span>
collecting ...
DevTools listening on ws://<span style="color:#d19a66">127.0</span>.<span style="color:#d19a66">0.1</span>:<span style="color:#d19a66">10351</span>/devtools/browser/<span style="color:#d19a66">78</span>bef34d<span style="color:#56b6c2">-b94c-4087-b724-34fb6b2ef6d1</span>
TestCase\test_search.py::TestSearch.test_001 ✓ <span style="color:#d19a66">50</span>% █████
TestCase\test_search.py::TestSearch.test_002 ✓ <span style="color:#d19a66">100</span>% ██████████
<span style="color:#56b6c2">-------------------------------</span> generated html file: file://C:\Users\hoou\PycharmProjects\web<span style="color:#56b6c2">-demotest</span>\report\report.html <span style="color:#56b6c2">--------------------------------</span>
Results (<span style="color:#d19a66">12.90</span>s):
<span style="color:#d19a66">2</span> passed
</code></span></span>
可以看到兩條用例已經執行成功了。
項目的report目錄中生成了一個report.html檔案。
這就是生成的測試報告檔案。
發送郵件
當項目執行完成之後,需要發送到自己或者其他人郵箱裡檢視結果。
我們編寫發送郵件的子產品。
在
utils
目錄中建立
send_mail.py
檔案
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> zmail
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">def</span> <span style="color:#61aeee">send_report</span>():
<span style="color:#98c379">"""發送報告"""</span>
<span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(cm.REPORT_FILE, encoding=<span style="color:#98c379">'utf-8'</span>) <span style="color:#7171bf">as</span> f:
content_html = f.read()
<span style="color:#7171bf">try</span>:
mail = {
<span style="color:#98c379">'from'</span>: <span style="color:#98c379">'[email protected]'</span>,
<span style="color:#98c379">'subject'</span>: <span style="color:#98c379">'最新的測試報告郵件'</span>,
<span style="color:#98c379">'content_html'</span>: content_html,
<span style="color:#98c379">'attachments'</span>: [cm.REPORT_FILE, ]
}
server = zmail.server(*cm.EMAIL_INFO.values())
server.send_mail(cm.ADDRESSEE, mail)
<span style="color:#7171bf">print</span>(<span style="color:#98c379">"測試郵件發送成功!"</span>)
<span style="color:#7171bf">except</span> Exception <span style="color:#7171bf">as</span> e:
<span style="color:#7171bf">print</span>(<span style="color:#98c379">"Error: 無法發送郵件,{}!"</span>, <span style="color:#7171bf">format</span>(e))
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">"__main__"</span>:
<span style="color:#98c379">'''請先在config/conf.py檔案設定QQ郵箱的賬号和密碼'''</span>
send_report()
</code></span></span>
執行該檔案:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">測試郵件發送成功!
</code></span></span>
可以看到測試報告郵件已經發送成功了。打開郵箱。
成功收到了郵件。
這個demo項目就算是整體完工了;是不是很有心得,在發送郵件的那一刻很有成就感。
最後,想必你已經對pytest+selenium架構有了一個整體的認知了,在自動化測試的道路上又上了一層台階。
pytest使用allure測試報告
- 選用的項目為
,在這個項目的基礎上說allure報告。Selenium自動化測試Pytest架構實戰
allure安裝
- 首先安裝python的allure-pytest包
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">pip install allure-pytest
</code></span></span>
- 然後安裝allure的command指令行程式
MacOS直接使用homebrew工具執行 brew install allure 即可安裝,不用配置下載下傳包和配置環境
在GitHub下載下傳安裝程式https://github.com/allure-framework/allure2/releases
但是由于GitHub通路太慢,我已經下載下傳好并放在了
群檔案
裡面
下載下傳完成後解壓放到一個檔案夾。我的路徑是
D:\Program Files\allure-2.13.3
然後配置環境變量: 在系統變量
path
中添加
D:\Program Files\allure-2.13.3\bin
,然後确定儲存。
打開cmd,輸入allure,如果結果顯示如下則表示成功了:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">C:\Users\hoou>allure
Usage: allure [<span style="color:#d19a66">options</span>] [<span style="color:#d19a66">command</span>] [<span style="color:#d19a66">command</span> <span style="color:#d19a66">options</span>]
Options:
<span style="color:#56b6c2">--help</span>
Print commandline help.
<span style="color:#56b6c2">-q</span>, <span style="color:#56b6c2">--quiet</span>
<span style="color:#7171bf">Switch</span> on the quiet mode.
Default: false
<span style="color:#56b6c2">-v</span>, <span style="color:#56b6c2">--verbose</span>
<span style="color:#7171bf">Switch</span> on the verbose mode.
Default: false
<span style="color:#56b6c2">--version</span>
Print commandline version.
Default: false
Commands:
generate Generate the report
Usage: generate [<span style="color:#d19a66">options</span>] The directories with allure results
Options:
<span style="color:#56b6c2">-c</span>, <span style="color:#56b6c2">--clean</span>
Clean Allure report directory before generating a new one.
Default: false
<span style="color:#56b6c2">--config</span>
Allure commandline config path. <span style="color:#7171bf">If</span> specified overrides values from
<span style="color:#56b6c2">--profile</span> and <span style="color:#56b6c2">--configDirectory</span>.
<span style="color:#56b6c2">--configDirectory</span>
Allure commandline configurations directory. By default uses
ALLURE_HOME directory.
<span style="color:#56b6c2">--profile</span>
Allure commandline configuration profile.
<span style="color:#56b6c2">-o</span>, <span style="color:#56b6c2">--report-dir</span>, <span style="color:#56b6c2">--output</span>
The directory to generate Allure report into.
Default: allure<span style="color:#56b6c2">-report</span>
serve Serve the report
Usage: serve [<span style="color:#d19a66">options</span>] The directories with allure results
Options:
<span style="color:#56b6c2">--config</span>
Allure commandline config path. <span style="color:#7171bf">If</span> specified overrides values from
<span style="color:#56b6c2">--profile</span> and <span style="color:#56b6c2">--configDirectory</span>.
<span style="color:#56b6c2">--configDirectory</span>
Allure commandline configurations directory. By default uses
ALLURE_HOME directory.
<span style="color:#56b6c2">-h</span>, <span style="color:#56b6c2">--host</span>
This host will be used to <span style="color:#7171bf">start</span> web server <span style="color:#7171bf">for</span> the report.
<span style="color:#56b6c2">-p</span>, <span style="color:#56b6c2">--port</span>
This port will be used to <span style="color:#7171bf">start</span> web server <span style="color:#7171bf">for</span> the report.
Default: <span style="color:#d19a66">0</span>
<span style="color:#56b6c2">--profile</span>
Allure commandline configuration profile.
open Open generated report
Usage: open [<span style="color:#d19a66">options</span>] The report directory
Options:
<span style="color:#56b6c2">-h</span>, <span style="color:#56b6c2">--host</span>
This host will be used to <span style="color:#7171bf">start</span> web server <span style="color:#7171bf">for</span> the report.
<span style="color:#56b6c2">-p</span>, <span style="color:#56b6c2">--port</span>
This port will be used to <span style="color:#7171bf">start</span> web server <span style="color:#7171bf">for</span> the report.
Default: <span style="color:#d19a66">0</span>
plugin Generate the report
Usage: plugin [<span style="color:#d19a66">options</span>]
Options:
<span style="color:#56b6c2">--config</span>
Allure commandline config path. <span style="color:#7171bf">If</span> specified overrides values from
<span style="color:#56b6c2">--profile</span> and <span style="color:#56b6c2">--configDirectory</span>.
<span style="color:#56b6c2">--configDirectory</span>
Allure commandline configurations directory. By default uses
ALLURE_HOME directory.
<span style="color:#56b6c2">--profile</span>
Allure commandline configuration profile.
</code></span></span>
allure初體驗
改造一下之前的測試用例代碼
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> re
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">import</span> allure
<span style="color:#7171bf">from</span> utils.logger <span style="color:#7171bf">import</span> log
<span style="color:#7171bf">from</span> common.readconfig <span style="color:#7171bf">import</span> ini
<span style="color:#7171bf">from</span> page_object.searchpage <span style="color:#7171bf">import</span> SearchPage
<span style="color:#61aeee">@allure.feature(<span style="color:#3388aa">"測試百度子產品"</span>)</span>
<span style="color:#7171bf">class</span> <span style="color:#61aeee">TestSearch</span>:
<span style="color:#61aeee"> @pytest.fixture(scope=<span style="color:#3388aa">'function'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">open_baidu</span>(self, drivers):
<span style="color:#98c379">"""打開百度"""</span>
search = SearchPage(drivers)
search.get_url(ini.url)
<span style="color:#61aeee"> @allure.story(<span style="color:#3388aa">"搜尋selenium結果用例"</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_001</span>(self, drivers):
<span style="color:#98c379">"""搜尋"""</span>
search = SearchPage(drivers)
search.input_search(<span style="color:#98c379">"selenium"</span>)
search.click_search()
result = re.search(<span style="color:#98c379">r'selenium'</span>, search.get_source)
log.info(result)
<span style="color:#7171bf">assert</span> result
<span style="color:#61aeee"> @allure.story(<span style="color:#3388aa">"測試搜尋候選用例"</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">test_002</span>(self, drivers):
<span style="color:#98c379">"""測試搜尋候選"""</span>
search = SearchPage(drivers)
search.input_search(<span style="color:#98c379">"selenium"</span>)
log.info(<span style="color:#7171bf">list</span>(search.imagine))
<span style="color:#7171bf">assert</span> <span style="color:#7171bf">all</span>([<span style="color:#98c379">"selenium"</span> <span style="color:#7171bf">in</span> i <span style="color:#7171bf">for</span> i <span style="color:#7171bf">in</span> search.imagine])
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">'__main__'</span>:
pytest.main([<span style="color:#98c379">'TestCase/test_search.py'</span>, <span style="color:#98c379">'--alluredir'</span>, <span style="color:#98c379">'./allure'</span>])
os.system(<span style="color:#98c379">'allure serve allure'</span>)
</code></span></span>
然後運作一下:
注意:如果你使用的是pycharm編輯器,請跳過該運作方式,直接使用
.bat
或
.sh
的方式運作
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-shell">***
------------------------------- generated html file: file://C:\Users\hoou\PycharmProjects\web-demotest\report.html --------------------------------
Results (12.97s):
2 passed
Generating report to temp directory...
Report successfully generated to C:\Users\hoou\AppData\Local\Temp\112346119265936111\allure-report
Starting web server...
2020-06-18 22:52:44.500:INFO::main: Logging initialized @1958ms to org.eclipse.jetty.util.log.StdErrLog
Server started at <http://172.18.47.241:6202/>. Press <Ctrl+C> to exit
</code></span></span>
指令行會出現如上提示,接着浏覽器會自動打開:
點選左下角
En
即可選擇切換為中文。
是不是很清爽很友好,比pytest-html更舒服。
allure裝飾器介紹
報告的生成和展示
剛才的兩個指令:
- 生成allure原始報告到report/allure目錄下,生成的全部為json或txt檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">pytest TestCase/test_search.py <span style="color:#56b6c2">--alluredir</span> ./allure
</code></span></span>
- 在一個臨時檔案中生成報告并啟動浏覽器打開
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">allure serve allure
</code></span></span>
但是在關閉浏覽器之後這個報告就再也打不開了。不建議使用這種。
是以我們必須使用其他的指令,讓allure可以指定生成的報告目錄。
我們在項目的根目錄中建立
run_case.py
檔案,内容如下:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> sys
<span style="color:#7171bf">import</span> subprocess
WIN = sys.platform.startswith(<span style="color:#98c379">'win'</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">main</span>():
<span style="color:#98c379">"""主函數"""</span>
steps = [
<span style="color:#98c379">"venv\\Script\\activate"</span> <span style="color:#7171bf">if</span> WIN <span style="color:#7171bf">else</span> <span style="color:#98c379">"source venv/bin/activate"</span>,
<span style="color:#98c379">"pytest --alluredir allure-results --clean-alluredir"</span>,
<span style="color:#98c379">"allure generate allure-results -c -o allure-report"</span>,
<span style="color:#98c379">"allure open allure-report"</span>
]
<span style="color:#7171bf">for</span> step <span style="color:#7171bf">in</span> steps:
subprocess.run(<span style="color:#98c379">"call "</span> + step <span style="color:#7171bf">if</span> WIN <span style="color:#7171bf">else</span> step, shell=<span style="color:#56b6c2">True</span>)
<span style="color:#7171bf">if</span> __name__ == <span style="color:#98c379">"__main__"</span>:
main()
</code></span></span>
指令釋義:
1、使用pytest生成原始報告,裡面大多數是一些原始的json資料,加入
--clean-alluredir
參數清除allure-results曆史資料。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">pytest <span style="color:#56b6c2">--alluredir</span> allure<span style="color:#56b6c2">-results</span> <span style="color:#56b6c2">--clean-alluredir</span>
</code></span></span>
- --clean-alluredir 清除allure-results曆史資料
2、使用generate指令導出HTML報告到新的目錄
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">allure generate allure<span style="color:#56b6c2">-results</span> <span style="color:#56b6c2">-o</span> allure<span style="color:#56b6c2">-report</span>
</code></span></span>
- -c 在生成報告之前先清理之前的報告目錄
- -o 指定生成報告的檔案夾
3、使用open指令在浏覽器中打開HTML報告
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">allure open allure<span style="color:#56b6c2">-report</span>
</code></span></span>
好了我們運作一下該檔案。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-powershell">Results (<span style="color:#d19a66">12.85</span>s):
<span style="color:#d19a66">2</span> passed
Report successfully generated to c:\Users\hoou\PycharmProjects\web<span style="color:#56b6c2">-demotest</span>\allure<span style="color:#56b6c2">-report</span>
Starting web server...
<span style="color:#d19a66">2020</span><span style="color:#56b6c2">-06-18</span> <span style="color:#d19a66">23</span>:<span style="color:#d19a66">30</span>:<span style="color:#d19a66">24.122</span>:INFO::main: Logging initialized @<span style="color:#d19a66">260</span>ms to org.eclipse.jetty.util.log.StdErrLog
Server started at <http://<span style="color:#d19a66">172.18</span>.<span style="color:#d19a66">47.241</span>:<span style="color:#d19a66">7932</span>/>. Press <Ctrl+C> to <span style="color:#7171bf">exit</span>
</code></span></span>
可以看到運作成功了。
在項目中的allure-report檔案夾也生成了相應的報告。
allure發生錯誤截圖
上面的用例全是運作成功的,沒有錯誤和失敗的,那麼發生了錯誤怎麼樣在allure報告中生成錯誤截圖呢,我們一起來看看。
首先我們先在
config/conf.py
檔案中添加一個截圖目錄和截圖檔案的配置。
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python">+++
<span style="color:#7171bf">class</span> <span style="color:#61aeee">ConfigManager</span>(<span style="color:#61aeee">object</span>):
...
<span style="color:#61aeee"> @property</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">screen_path</span>(self):
<span style="color:#98c379">"""截圖目錄"""</span>
screenshot_dir = os.path.join(self.BASE_DIR, <span style="color:#98c379">'screen_capture'</span>)
<span style="color:#7171bf">if</span> <span style="color:#7171bf">not</span> os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
now_time = dt_strftime(<span style="color:#98c379">"%Y%m%d%H%M%S"</span>)
screen_file = os.path.join(screenshot_dir, <span style="color:#98c379">"{}.png"</span>.<span style="color:#7171bf">format</span>(now_time))
<span style="color:#7171bf">return</span> now_time, screen_file
...
+++
</code></span></span>
然後我們修改項目目錄中的
conftest.py
:
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"><span style="color:#5c6370"><em>#!/usr/bin/env python3</em></span>
<span style="color:#5c6370"><em># -*- coding:utf-8 -*-</em></span>
<span style="color:#7171bf">import</span> base64
<span style="color:#7171bf">import</span> pytest
<span style="color:#7171bf">import</span> allure
<span style="color:#7171bf">from</span> py.xml <span style="color:#7171bf">import</span> html
<span style="color:#7171bf">from</span> selenium <span style="color:#7171bf">import</span> webdriver
<span style="color:#7171bf">from</span> config.conf <span style="color:#7171bf">import</span> cm
<span style="color:#7171bf">from</span> common.readconfig <span style="color:#7171bf">import</span> ini
<span style="color:#7171bf">from</span> utils.times <span style="color:#7171bf">import</span> timestamp
<span style="color:#7171bf">from</span> utils.send_mail <span style="color:#7171bf">import</span> send_report
driver = <span style="color:#56b6c2">None</span>
<span style="color:#61aeee">@pytest.fixture(scope=<span style="color:#3388aa">'session'</span>, autouse=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">drivers</span>(request):
<span style="color:#7171bf">global</span> driver
<span style="color:#7171bf">if</span> driver <span style="color:#7171bf">is</span> <span style="color:#56b6c2">None</span>:
driver = webdriver.Chrome()
driver.maximize_window()
<span style="color:#7171bf">def</span> <span style="color:#61aeee">fn</span>():
driver.quit()
request.addfinalizer(fn)
<span style="color:#7171bf">return</span> driver
<span style="color:#61aeee">@pytest.hookimpl(hookwrapper=<span style="color:#56b6c2">True</span>)</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_runtest_makereport</span>(item):
<span style="color:#98c379">"""
當測試失敗的時候,自動截圖,展示到html報告中
:param item:
"""</span>
pytest_html = item.config.pluginmanager.getplugin(<span style="color:#98c379">'html'</span>)
outcome = <span style="color:#7171bf">yield</span>
report = outcome.get_result()
report.description = <span style="color:#7171bf">str</span>(item.function.__doc__)
extra = <span style="color:#7171bf">getattr</span>(report, <span style="color:#98c379">'extra'</span>, [])
<span style="color:#7171bf">if</span> report.when == <span style="color:#98c379">'call'</span> <span style="color:#7171bf">or</span> report.when == <span style="color:#98c379">"setup"</span>:
xfail = <span style="color:#7171bf">hasattr</span>(report, <span style="color:#98c379">'wasxfail'</span>)
<span style="color:#7171bf">if</span> (report.skipped <span style="color:#7171bf">and</span> xfail) <span style="color:#7171bf">or</span> (report.failed <span style="color:#7171bf">and</span> <span style="color:#7171bf">not</span> xfail):
screen_img = _capture_screenshot()
<span style="color:#7171bf">if</span> screen_img:
html = <span style="color:#98c379">'<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:1024px;height:768px;" '</span> \
<span style="color:#98c379">'onclick="window.open(this.src)" align="right"/></div>'</span> % screen_img
extra.append(pytest_html.extras.html(html))
report.extra = extra
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_header</span>(cells):
cells.insert(<span style="color:#d19a66">1</span>, html.th(<span style="color:#98c379">'用例名稱'</span>))
cells.insert(<span style="color:#d19a66">2</span>, html.th(<span style="color:#98c379">'Test_nodeid'</span>))
cells.pop(<span style="color:#d19a66">2</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_row</span>(report, cells):
cells.insert(<span style="color:#d19a66">1</span>, html.td(report.description))
cells.insert(<span style="color:#d19a66">2</span>, html.td(report.nodeid))
cells.pop(<span style="color:#d19a66">2</span>)
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_table_html</span>(report, data):
<span style="color:#7171bf">if</span> report.passed:
<span style="color:#7171bf">del</span> data[:]
data.append(html.div(<span style="color:#98c379">'通過的用例未捕獲日志輸出.'</span>, class_=<span style="color:#98c379">'empty log'</span>))
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_report_title</span>(report):
report.title = <span style="color:#98c379">"pytest示例項目測試報告"</span>
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_configure</span>(config):
config._metadata.clear()
config._metadata[<span style="color:#98c379">'測試項目'</span>] = <span style="color:#98c379">"測試百度官網搜尋"</span>
config._metadata[<span style="color:#98c379">'測試位址'</span>] = ini.url
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_html_results_summary</span>(prefix, summary, postfix):
<span style="color:#5c6370"><em># prefix.clear() # 清空summary中的内容</em></span>
prefix.extend([html.p(<span style="color:#98c379">"所屬部門: XX公司測試部"</span>)])
prefix.extend([html.p(<span style="color:#98c379">"測試執行人: 随風揮手"</span>)])
<span style="color:#7171bf">def</span> <span style="color:#61aeee">pytest_terminal_summary</span>(terminalreporter, exitstatus, config):
<span style="color:#98c379">"""收集測試結果"""</span>
result = {
<span style="color:#98c379">"total"</span>: terminalreporter._numcollected,
<span style="color:#98c379">'passed'</span>: <span style="color:#7171bf">len</span>(terminalreporter.stats.get(<span style="color:#98c379">'passed'</span>, [])),
<span style="color:#98c379">'failed'</span>: <span style="color:#7171bf">len</span>(terminalreporter.stats.get(<span style="color:#98c379">'failed'</span>, [])),
<span style="color:#98c379">'error'</span>: <span style="color:#7171bf">len</span>(terminalreporter.stats.get(<span style="color:#98c379">'error'</span>, [])),
<span style="color:#98c379">'skipped'</span>: <span style="color:#7171bf">len</span>(terminalreporter.stats.get(<span style="color:#98c379">'skipped'</span>, [])),
<span style="color:#5c6370"><em># terminalreporter._sessionstarttime 會話開始時間</em></span>
<span style="color:#98c379">'total times'</span>: timestamp() - terminalreporter._sessionstarttime
}
<span style="color:#7171bf">print</span>(result)
<span style="color:#7171bf">if</span> result[<span style="color:#98c379">'failed'</span>] <span style="color:#7171bf">or</span> result[<span style="color:#98c379">'error'</span>]:
send_report()
<span style="color:#7171bf">def</span> <span style="color:#61aeee">_capture_screenshot</span>():
<span style="color:#98c379">"""截圖儲存為base64"""</span>
now_time, screen_file = cm.screen_path
driver.save_screenshot(screen_file)
allure.attach.file(screen_file,
<span style="color:#98c379">"失敗截圖{}"</span>.<span style="color:#7171bf">format</span>(now_time),
allure.attachment_type.PNG)
<span style="color:#7171bf">with</span> <span style="color:#7171bf">open</span>(screen_file, <span style="color:#98c379">'rb'</span>) <span style="color:#7171bf">as</span> f:
imagebase64 = base64.b64encode(f.read())
<span style="color:#7171bf">return</span> imagebase64.decode()
</code></span></span>
來看看我們修改了什麼:
- 我們修改了_capture_screenshot函數
在裡面我們使用了webdriver截圖生成檔案,并使用allure.attach.file方法将檔案添加到了allure測試報告中。
并且我們還傳回了圖檔的base64編碼,這樣可以讓pytest-html的錯誤截圖和allure都能生效。
運作一次得到兩份報告,一份是簡單的一份是好看内容豐富的。
現在我們在測試用例中建構一個預期的錯誤測試一個我們的這個代碼。
修改test_002測試用例
<span style="color:#596172"><span style="background-color:#ffffff"><code class="language-python"> <span style="color:#7171bf">assert</span> <span style="color:#7171bf">not</span> <span style="color:#7171bf">all</span>([<span style="color:#98c379">"selenium"</span> <span style="color:#7171bf">in</span> i <span style="color:#7171bf">for</span> i <span style="color:#7171bf">in</span> search.imagine])
</code></span></span>
運作一下:
可以看到allure報告中已經有了這個錯誤的資訊。
再來看看pytest-html中生成的報告:
可以看到兩份生成的報告都附帶了錯誤的截圖,真是魚和熊掌可以兼得呢。
好了,到這裡可以說allure的報告就先到這裡了,以後發現allure其他的精彩之處我再來分享。
開源位址
為了友善學習交流,本次的示例項目已經儲存在網盤了,直接私信000