前言
selenium自動化+unittest測試框架
- 本章你需要
-
一定的python基礎——至少明白類與對象,封裝繼承
-
一定的selenium基礎——不講selenium,不會的自己去看selenium中文翻譯網
-
項目框架
開始之前先簡單介紹一下框架體系吧:
目錄/文件 | 說明 | 是否為python包 |
---|---|---|
common | 常見的通用類。如:讀取config文件,元素文件 | 是 |
config | 配置文件目錄 | 是 |
logs | 日志文件目錄 | |
page | selenium基類 | 是 |
page_element | 頁面元素 | |
page_object | 頁面對象POM設計模式,本人對這個的理解來自於苦葉子的博客 | 是 |
report | 報告目錄 | |
TestCase | 測試用例 | 是 |
utils | 工具類。如:郵箱、日志 | 是 |
run_case.py | 總執行文件 | |
script | 常用腳本目錄 |
- 框架有什么優點呢?
- 代碼復用率高,如果不使用框架的話,代碼會很冗余
- 可以組裝日志、報告、郵件等一些高級功能
- 提高元素等數據的可維護性,元素發生變化時,只需要更新一下配置文件
- 使用更靈活的PageObject設計模式
知道了以上這些我們就開始吧!
我們在項目中先按照上面的框架指引,建好每一項目錄。
注意:python包為是的,都需要添加一個__init__.py
文件以標識此目錄為一個python包。
首先管理時間
首先呢,因為用到時間的地方可能會比較多,所以我們也單獨把時間封裝成一個模塊。讓其他模塊來調用即可。
在`utils`目錄中新建`times.py`文件。填入以下內容。
```python
!/usr/bin/env python3
-- coding:utf-8 --
import time
import datetime
from functools import wraps
def timestamp():
"""時間戳"""
return time.time()
def datetime_strftime(fmt="%Y%m"):
"""datetime格式化時間"""
return datetime.datetime.now().strftime(fmt)
def sleep(seconds=1.0):
"""
睡眠時間
"""
time.sleep(seconds)
def run_time(func):
"""運行時長"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = timestamp()
res = func(*args, **kwargs)
print("Done!用時%.3f秒!" % (timestamp() - start_time))
return res
return wrapper
if name == 'main':
print(timestamp())
```
配置文件
配置文件總是一個項目的重要組成部分!
在項目中我們創建config
文件目錄,里面存放我們本次項目所需要的配置文件。包括我們常用的目錄管理和一些設置。
conf.py
在項目config
目錄創建conf.py
文件,所有的目錄配置或者基本不變的配置信息寫在這個文件里面。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
from selenium.webdriver.common.by import By
from utils.times import datetime_strftime
class ConfigManager(object):
# 項目目錄
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 配置文件
INI_PATH = os.path.join(BASE_DIR, 'config', 'config.ini')
# 頁面元素目錄
ELEMENT_PATH = os.path.join(BASE_DIR, 'page_element')
# 報告目錄
REPORT_PATH = os.path.join(BASE_DIR, 'report')
# 測試用例
TEST_SUITES = os.path.join(BASE_DIR, "TestCase")
# 元素定位的類型
LOCATE_MODE = {
'css': By.CSS_SELECTOR,
'xpath': By.XPATH,
'name': By.NAME,
'id': By.ID,
'class': By.CLASS_NAME
}
# 郵件信息
EMAIL_INFO = {
'username': '1084502012@qq.com', # 切換成你自己的地址
'password': 'QQ郵箱授權碼',
'smtp_host': 'smtp.qq.com',
'smtp_port': 465
}
# 收件人
ADDRESSEE = ['1084502012@qq.com', ]
@property
def log_path(self):
# 日志目錄
log_path = os.path.join(self.BASE_DIR, 'logs')
if not os.path.exists(log_path):
os.makedirs(log_path)
return os.path.join(log_path, '{}.log'.format(datetime_strftime()))
@property
def report_path(self):
"""報告文件"""
_path = self.REPORT_PATH
if not os.path.exists(_path):
os.makedirs(_path)
return os.path.join(_path, '{}.html'.format(datetime_strftime("%Y%m%d_%H%M%S")))
@property
def get_new_report(self):
"""獲取最新的報告"""
_path = self.REPORT_PATH
report_path = os.listdir(_path)
report_new_path = sorted(report_path,
key=lambda x: os.path.getmtime(os.path.join(_path, x)))
report_new_file = os.path.join(_path, report_new_path[-1])
with open(report_new_file, encoding='utf-8') as f:
return f.read()
cm = ConfigManager()
if __name__ == '__main__':
print(cm.BASE_DIR)
注意:QQ郵箱授權碼:點擊查看生成教程
這個conf文件我模仿了Django的settings.py文件風格。
在這個文件中我們可以設置自己的各個目錄,也可以查看自己當前的目錄。
遵循了約定:不變的常量名全部大寫,變量名稱小寫的規范。看起來整體美觀。
config.ini
然后在config
目錄中新建一個config.ini
文件,里面暫時先放入我們的需要測試的URL
[HOST]
HOST = https://www.baidu.com
配置文件創建好了,接下來我們需要讀取這個配置文件以使用里面的信息。
讀取配置文件
在common
目錄新建一個readconfig.py
文件。這個文件就是我們用來讀取config.ini
文件里面的配置信息的。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
import configparser
from config.conf import cm
HOST = 'HOST'
class ReadConfig:
"""配置文件"""
def __init__(self):
self.path = cm.INI_PATH
if not os.path.exists(self.path):
raise FileNotFoundError("配置文件%s不存在!" % self.path)
self.config = configparser.RawConfigParser() # 當有%的符號時請使用Raw讀取
self.config.read(self.path, encoding='utf-8')
def _get(self, section, option):
"""獲取"""
return self.config.get(section, option)
def _set(self, section, option, value):
"""更新"""
self.config.set(section, option, value)
with open(self.path, 'w') as f:
self.config.write(f)
@property
def url(self):
return self._get(HOST, HOST)
ini = ReadConfig()
if __name__ == '__main__':
print(ini.url)
可以看到我們用python內置的configparser模塊對config.ini
文件進行了讀取。
對於url值的提取,我使用了高階語法@property將方法改寫為屬性值,調用更簡單。
記錄操作日志
日志,大家應該都很熟悉這個名詞,就是記錄代碼中的動作。
在utils
目錄中新建logger.py
文件。
這個文件就是我們用來在自動化測試過程中記錄一些操作步驟的。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import logging
from config.conf import cm
class Logger:
def __init__(self, name=None):
self.logger = logging.getLogger(name)
if not self.logger.handlers:
self.logger.setLevel(logging.DEBUG)
# 創建一個handle寫入文件
fh = logging.FileHandler(cm.log_path, encoding='utf-8')
fh.setLevel(logging.INFO)
# 創建一個handle輸出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 定義輸出的格式
formatter = logging.Formatter(self.fmt)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 添加到handle
self.logger.addHandler(fh)
self.logger.addHandler(ch)
@property
def fmt(self):
return '%(levelname)s\t%(asctime)s\t%(name)s:%(lineno)d\t%(message)s'
if __name__ == '__main__':
log = Logger(__name__).logger
log.info('hello world')
在終端中運行該文件,就看到命令行打印出了:
INFO 2020-1-16 20:37:19,726 __main__:37 hello world
然后在項目logs
目錄下生成了當月的日志文件。
話不多說,進入正題!
簡單理解POM模型
由於下面要講元素相關的,所以首先理解一下POM模型
Page Object模式具有以下幾個優點。
該觀點來自 《Selenium自動化測試——基於Python語言》
- 抽象出對象可以最大程度地降低開發人員修改頁面代碼對測試的影響, 所以, 你僅需要對頁
面對象進行調整, 而對測試沒有影響; - 可以在多個測試用例中復用一部分測試代碼;
- 測試代碼變得更易讀、 靈活、 可維護
Page Object模式圖
- basepage ——selenium的基類,對selenium的方法進行封裝
- pageelements——頁面元素,把頁面元素單獨提取出來,放入一個文件中
- searchpage ——頁面對象類,把selenium方法和頁面元素進行整合
- testcase ——使用pytest對整合的searchpage進行測試用例編寫
通過上圖我們可以看出,通過POM模型思想,我們把:
-
selenium方法
-
頁面元素
-
頁面對象
-
測試用例
以上四種代碼主體進行了拆分,雖然在用例很少的情況下做會增加代碼,但是當用例多的時候意義很大,代碼量會在用例增加的時候顯著減少。我們維護代碼變得更加直觀明顯,代碼可讀性也變得比工廠模式強很多,代碼復用率也極大的得到了提高。
管理頁面元素
本教程選擇的測試地址是百度首頁,所以對應的元素也是百度首頁的。
項目框架設計中有一個目錄page_element
就是專門來存放定位元素的文件的。
通過對各種配置文件的對比,我在這里選擇的是YAML文件格式。其支持與python數據類型堪稱無縫對接,且易讀,交互性好。
我們在page_element
目錄中新建一個search.yaml
文件。
搜索框: "id==kw"
候選: "css==.bdsug-overflow"
搜索候選: "css==#form div li"
搜索按鈕: "id==su"
元素定位文件創建好了,下來我們需要讀取這個文件。
在common
中創建readelement.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
import yaml
from config.conf import cm
class Element:
"""獲取元素"""
def __init__(self, name):
self.file_name = '%s.yaml' % name
self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
if not os.path.exists(self.element_path):
raise FileNotFoundError("%s 文件不存在!" % self.element_path)
with open(self.element_path, encoding='utf-8') as f:
self.data = yaml.safe_load(f)
def __getitem__(self, item):
"""獲取屬性"""
data = self.data.get(item)
if data:
name, value = data.split('==')
return name, value
raise ArithmeticError("{}中不存在關鍵字:{}".format(self.file_name, item))
if __name__ == '__main__':
search = Element('search')
print(search['搜索框'])
通過特殊方法__getitem__
實現調用任意屬性,讀取yaml中的值。
這樣我們就實現了定位元素的存儲和調用。
但是還有一個問題,我們怎么樣才能確保我們寫的每一項元素不出錯,人為的錯誤是不可避免的,但是我們可以通過代碼來運行對文件的審查。但是也不能檢查出所有的問題,所以還是要細心。
所以我們編寫一個文件,在script
目錄中創建inspect.py
文件,對所有的元素yaml文件進行一次大概的審查。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
import yaml
from utils.times import run_time
from config.conf import cm
@run_time
def inspect_element():
"""審查所有的元素是否正確"""
for i in os.listdir(cm.ELEMENT_PATH):
_path = os.path.join(cm.ELEMENT_PATH, i)
if os.path.isfile(_path):
with open(_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
for k in data.values():
pattern, value = k.split('==')
if pattern not in cm.LOCATE_MODE:
raise AttributeError('【%s】路徑中【%s]元素沒有指定類型' % (i, k))
if pattern == 'xpath':
assert '//' in value, '【%s】路徑中【%s]元素xpath類型與值不配' % (
i, k)
if pattern == 'css':
assert '//' not in value, '【%s】路徑中【%s]元素css類型與值不配' % (
i, k)
if pattern in ('id', 'name', 'class'):
assert value, '【%s】路徑中【%s]元素類型與值不匹配' % (i, k)
if __name__ == '__main__':
inspect_element()
執行該文件:
Done!用時0.015秒!
可以看到,很短的時間內,我們就對所填寫的YAML文件進行了審查。
封裝Selenium基類
在工廠模式種我們是這樣寫的:
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://www.baidu.com')
driver.find_element_by_xpath("//input[@id='kw']").send_keys('selenium')
driver.find_element_by_xpath("//input[@id='su']").click()
time.sleep(5)
driver.quit()
很直白,簡單,又明了。
創建driver對象,打開百度網頁,搜索selenium,點擊搜索,然后停留5秒,查看結果,最后關閉瀏覽器。
那我們為什么要封裝selenium的方法呢。首先我們上述這種較為原始的方法,基本不適用於平時做UI自動化測試的,因為在UI界面實際運行情況遠遠比較復雜,可能因為網絡原因,或者控件原因,我們元素還沒有顯示出來,就進行點擊或者輸入。所以我們需要封裝selenium方法,通過內置的顯式等待或一定的條件語句,才能構建一個穩定的方法。而且把selenium方法封裝起來,有利於平時的代碼維護。
我們在page
目錄創建webpage.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
selenium基類
本文件存放了selenium基類的封裝方法
"""
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from config.conf import cm
from utils.times import sleep
from utils.logger import Logger
log = Logger(__name__).logger
class WebPage(object):
"""selenium基類"""
def __init__(self, driver):
# self.driver = webdriver.Chrome()
self.driver = driver
self.timeout = 20 # 查找元素的超時時間
self.wait = WebDriverWait(self.driver, self.timeout)
def get_url(self, url):
"""打開網址並驗證"""
self.driver.maximize_window()
self.driver.set_page_load_timeout(60)
try:
self.driver.get(url)
self.driver.implicitly_wait(10)
log.info("打開網頁:%s" % url)
except TimeoutException:
raise TimeoutException("打開%s超時請檢查網絡或網址服務器" % url)
@staticmethod
def element_locator(func, locator):
"""元素定位器"""
name, value = locator
return func(cm.ELEMENT_PATH[name], value)
def find_element(self, locator):
"""尋找單個元素"""
return WebPage.element_locator(lambda *args: self.wait.until(
EC.presence_of_element_located(args)), locator)
def find_elements(self, locator):
"""查找多個相同的元素"""
return WebPage.element_locator(lambda *args: self.wait.until(
EC.presence_of_all_elements_located(args)), locator)
def ele_num(self, locator): # 獲取相同元素的個數
"""獲取相同元素的個數"""
number = len(self.find_elements(locator))
log.info("相同元素:{}".format((locator, number)))
return number
def input_text(self, locator, txt):
"""輸入(輸入前先清空)"""
sleep(0.5)
ele = self.find_element(locator)
ele.clear()
ele.send_keys(txt)
log.info("輸入文本:{}".format(txt))
def is_click(self, locator):
"""點擊"""
self.find_element(locator).click()
sleep()
log.info("點擊元素:{}".format(locator))
def get_text(self, locator):
"""獲取當前的text"""
ele_text = self.find_element(locator).text
log.info("獲取文本:{}".format(ele_text))
return ele_text
@property
def get_source(self):
"""獲取頁面源代碼"""
return self.driver.page_source
def shot_file(self, path):
"""文件截圖"""
return self.driver.save_screenshot(path)
def refresh(self):
"""刷新頁面F5"""
self.driver.refresh()
self.driver.implicitly_wait(30)
if __name__ == "__main__":
pass
在文件中我們對主要用了顯式等待對selenium的click,send_keys等方法,做了二次封裝。提高了運行的成功率。
好了我們完成了POM模型的一半工作了。接下來我們們進入頁面對象。
創建頁面對象
我們開始創建searchpage頁面對象類。
在page_object
目錄下創建一個searchpage.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from page.webpage import WebPage, sleep
from common.readelement import Element
search = Element('search')
class SearchPage(WebPage):
"""搜索類"""
def input_search(self, content):
"""輸入搜索"""
self.input_text(search['搜索框'], txt=content)
sleep()
@property
def imagine(self):
"""搜索聯想"""
return [x.text for x in self.find_elements(search['候選'])]
def click_search(self):
"""點擊搜索"""
self.is_click(search['搜索按鈕'])
if __name__ == '__main__':
pass
在該文件中我們對,輸入搜索關鍵詞,點擊搜索,搜索聯想,進行了封裝。
並配置了注釋。
在平時中我們應該養成寫注釋的習慣,因為過一段時間后,沒有注釋,代碼讀起來很費勁。
好了我們的頁面對象此時業已完成了。
下面我們開始編寫測試用例。
熟悉unittest測試框架
unittest是python標准庫中自帶的測試框架。
學習unittest最好的教程應該是python官方文檔了。
官方文檔:https://docs.python.org/zh-cn/3.7/library/unittest.html
不細說了,自己去看吧。
編寫測試用例
我們將使用unittest
編寫測試用例。
在TestCase
目錄中創建test_search.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re
import unittest
from selenium import webdriver
from utils.logger import Logger
from common.readconfig import ini
from page_object.searchpage import SearchPage
from chromedriver_py import binary_path
log = Logger(__name__).logger
class TestSearch(unittest.TestCase):
"""搜索測試"""
@classmethod
def setUpClass(cls) -> None:
cls.driver = webdriver.Chrome(executable_path=binary_path)
cls.search = SearchPage(cls.driver)
cls.search.get_url(ini.url)
@classmethod
def tearDownClass(cls) -> None:
cls.driver.quit()
def setUp(self) -> None:
self.imgs = [] # 注意:添加此行用於報告中失敗截圖
def test_001(self):
"""搜索"""
self.search.input_search("selenium")
self.search.click_search()
result = re.search(r'selenium', self.search.get_source)
log.info(result)
assert result
def test_002(self):
"""測試搜索候選"""
self.search.input_search("selenium")
log.info(list(self.search.imagine))
assert all(["selenium" in i for i in self.search.imagine])
if __name__ == '__main__':
unittest.main(verbosity=2)
我們測試用例就編寫好了。
- 第一個測試用例:
- 我們實現了在百度selenium關鍵字,並點擊搜索按鈕,並在搜索結果中,用正則查找結果頁源代碼,返回數量大於10我們就認為通過。
- 第二個測試用例:
- 我們實現了,搜索selenium,然后斷言搜索候選中的所有結果有沒有selenium關鍵字。
執行用例
以上我們已經編寫完成了整個框架和測試用例。
我們進入到當前項目的主目錄執行文件:
DevTools listening on ws://127.0.0.1:7341/devtools/browser/09ec4049-0745-4048-8fe2-ece53457d8c0
INFO 2020-06-10 16:21:20,021 [webpage.py:37] 打開網頁:https://www.baidu.com
test_001 (__main__.TestSearch)
搜索 ... INFO 2020-06-10 16:21:20,667 [webpage.py:69] 輸入文本:selenium
INFO 2020-06-10 16:21:22,738 [webpage.py:75] 點擊元素:('id', 'su')
INFO 2020-06-10 16:21:22,785 [test_search.py:43] <re.Match object; span=(326, 334), match='selenium'>
ok
test_002 (__main__.TestSearch)
測試搜索候選 ... INFO 2020-06-10 16:21:23,407 [webpage.py:69] 輸入文本:selenium
INFO 2020-06-10 16:21:24,466 [test_search.py:49] ['selenium', 'selenium python', 'selenium面試題', 'selenium中文文檔']
ok
----------------------------------------------------------------------
Ran 2 tests in 12.531s
OK
可以看到兩條用例已經執行成功了。
生成測試報告
unittest無法像pytest一樣直接添加命令行參數就可以生成報告。
但是可以通過第三方模塊HTMLTestRunner_cn去生成報告。我們需要下載該模塊去生成報告。
生成的報告結果圖:
這個模塊我已保存在群文件中。左上角加群。
然后將下載的HTMLTestRunner_cn.py文件放在項目的tools
目錄。
執行並生成報告
我們在項目根目錄下創建run_case.py文件作為項目的運行文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import unittest
from config.conf import cm
from utils.send_mail import send_report_mail
from utils.HTMLTestRunner_cn import HTMLTestRunner
discover = unittest.defaultTestLoader.discover(cm.TEST_SUITES, pattern="test*.py")
def main():
"""主函數"""
try:
with open(cm.report_path, 'wb+') as fp:
runner = HTMLTestRunner(stream=fp,
title="測試結果",
description="用例執行情況",
verbosity=2,
retry=1,
save_last_try=True)
result = runner.run(discover)
except Exception as e:
print("用例執行失敗:{}".format(e))
else:
if result.failure_count or result.error_count:
# 如果測試有錯誤,就發送郵件
send_report_mail()
if __name__ == "__main__":
main()
發送結果郵件
當項目執行完成之后,需要發送到自己或者其他人郵箱里查看結果。
我們編寫發送郵件的模塊。
在tools
目錄中新建send_mail.py
文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import smtplib
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
from email.mime.multipart import MIMEMultipart
from config.conf import cm
def _format_addr(s):
"""格式化郵件地址"""
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
def send_report_mail():
"""發送最新的測試報告"""
# email地址和口令:
email_info = cm.EMAIL_INFO
user = email_info['username']
pwd = email_info['password']
# 收件人地址
to_addr = cm.ADDRESSEE
# SMTP服務器地址
smtp_server = email_info['smtp_host']
smtp_port = email_info['smtp_port']
try:
# 初始化郵件對象
msg = MIMEMultipart()
msg['From'] = _format_addr("selenium愛好者<%s>" % user)
msg['To'] = _format_addr('管理員 <%s>' % ','.join(to_addr))
msg['Subject'] = Header("unittest演示測試最新的測試報告", 'utf-8').encode()
# 發送HTML文件
msg.attach(MIMEText(cm.get_new_report, 'html', 'utf-8'))
# 發件人郵箱中的SMTP服務器,端口
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
# 括號中對應的是發件人郵箱賬號、郵箱密碼
server.login(user, pwd)
# 括號中對應的是發件人郵箱賬號、收件人郵箱賬號、發送郵件
server.sendmail(user, to_addr, msg.as_string())
print("測試結果郵件發送成功!")
except smtplib.SMTPException as e:
print(u"Error: 無法發送郵件", format(e))
if __name__ == '__main__':
send_report_mail()
執行該文件:
測試郵件發送成功!
可以看到測試報告郵件已經發送成功了。
打開郵箱。
成功收到了郵件,只是樣式丟失了。
這個unittest框架的selenium—demo項目就算是整體完工了。
開源地址
為了方便學習交流,本次的示例項目已經開源在碼雲:
https://gitee.com/wxhou/web-unittest