從零搭建完整python自動(dòng)化測(cè)試框架(UI自動(dòng)化和接口自動(dòng)化)
文章目錄
總體框架
PO模式、DDT數(shù)據(jù)驅(qū)動(dòng)、關(guān)鍵字驅(qū)動(dòng)
框架技術(shù)選擇
框架運(yùn)行結(jié)果
各用例對(duì)應(yīng)的定義方式(PO/DDT)
測(cè)試執(zhí)行結(jié)果
從零開(kāi)始搭建項(xiàng)目
一、開(kāi)發(fā)環(huán)境搭建
二、新建項(xiàng)目
三、基礎(chǔ)功能實(shí)現(xiàn)
1. 配置功能實(shí)現(xiàn)(Conf)
2. 日志功能實(shí)現(xiàn)(Log)
3. 讀取EXCEL實(shí)現(xiàn)(data)
4. 郵件發(fā)送實(shí)現(xiàn)(Email)
四、WEB UI自動(dòng)化
1. 頁(yè)面PO對(duì)象配置
2. 實(shí)現(xiàn)basePage基類(lèi)
3. 寫(xiě)業(yè)務(wù)測(cè)試用例
五、實(shí)現(xiàn)主程序
1. API對(duì)象配置
2.實(shí)現(xiàn)base_api基類(lèi)
3.測(cè)試用例
補(bǔ):MD5函數(shù)
結(jié)語(yǔ)
本自動(dòng)化測(cè)試框架采用python + unittest 的基礎(chǔ)來(lái)搭建,采用PO模式、數(shù)據(jù)驅(qū)動(dòng)的思想,通過(guò)selenium來(lái)實(shí)現(xiàn)WEB UI自動(dòng)化,通過(guò)request來(lái)實(shí)現(xiàn)接口自動(dòng)化。移動(dòng)終端的自動(dòng)化也可在該框架基礎(chǔ)上去構(gòu)建補(bǔ)充。
總體框架
總體框架如下圖:
用例掃描、測(cè)試結(jié)果反饋,如要和其它項(xiàng)目管理系統(tǒng)或是用例管理系統(tǒng)對(duì)接(比如testlink),就需要單獨(dú)出來(lái)進(jìn)行處理。
對(duì)于大型的產(chǎn)品,用例數(shù)特別多的話(huà),需要建設(shè)一個(gè)master節(jié)點(diǎn),專(zhuān)門(mén)負(fù)責(zé)管理用例和腳本,分發(fā)測(cè)試腳本,指定測(cè)試環(huán)境,匯總測(cè)試結(jié)果等。各節(jié)點(diǎn)執(zhí)行分給自己的測(cè)試用例即可。
PO模式、DDT數(shù)據(jù)驅(qū)動(dòng)、關(guān)鍵字驅(qū)動(dòng)
PO模式(Page Object)是UI自動(dòng)化測(cè)試常采用的一種設(shè)計(jì)模式,用于解決開(kāi)發(fā)頻繁修改UI頁(yè)面而導(dǎo)致的自動(dòng)化腳本維護(hù)困難的問(wèn)題。
PO模式中心思想:
- 每一個(gè)頁(yè)面為一個(gè)對(duì)象;
- 每一個(gè)對(duì)象維護(hù)著頁(yè)面中的各元素和操作方法;
- 用例測(cè)試腳本只需要聚集業(yè)務(wù)邏輯和測(cè)試數(shù)據(jù);
- UI頁(yè)面的變更,只需要修改對(duì)應(yīng)的PO對(duì)象,無(wú)需修改測(cè)試腳本(理想情況下。實(shí)際上也很難100%做到,因?yàn)閁I的變更很多時(shí)候意味著業(yè)務(wù)邏輯的變更)。
DDT(Data Driven Testing)數(shù)據(jù)驅(qū)動(dòng)測(cè)試模式,用來(lái)解決部分自動(dòng)化用例邏輯完全相同,只有測(cè)試數(shù)據(jù)和預(yù)期結(jié)果不同的問(wèn)題。實(shí)際上就是同一測(cè)試腳本使用不同的測(cè)試數(shù)據(jù)來(lái)反復(fù)執(zhí)行(但腳本只需要寫(xiě)一個(gè)),測(cè)試數(shù)據(jù)和測(cè)試行為完全分離。
DDT中心思想:
- 將測(cè)試數(shù)據(jù)分離出來(lái),單獨(dú)維護(hù);
- 減少重復(fù)自動(dòng)化用例的數(shù)量。
將以上兩種思想進(jìn)行結(jié)合,就可以做成?對(duì)象、數(shù)據(jù)、業(yè)務(wù)行為 三者分離的模型,再結(jié)合模塊進(jìn)行管理,為后續(xù)自動(dòng)化用例腳本的長(zhǎng)期維護(hù)打下基礎(chǔ)。否則時(shí)間一長(zhǎng)自動(dòng)化就會(huì)亂成一團(tuán),維護(hù)成本越來(lái)越高,陷入自動(dòng)化率不升反降的怪圈。
關(guān)鍵字驅(qū)動(dòng)(Keyword Driven Testing),在前面的基礎(chǔ)上,可以進(jìn)一步實(shí)現(xiàn)關(guān)鍵字驅(qū)動(dòng)。即將業(yè)務(wù)邏輯相同的部分,抽象成關(guān)鍵字庫(kù)。這樣在寫(xiě)自動(dòng)化用例腳本時(shí),只需要寫(xiě)關(guān)鍵字和對(duì)應(yīng)測(cè)試數(shù)據(jù)即可,可以進(jìn)一步減少工作量,減少測(cè)試人員對(duì)代碼的學(xué)習(xí)和依賴(lài)。
如京東搜索商品時(shí)直接寫(xiě)腳本需要好多步:
- 定位到搜索框
- 輸入關(guān)鍵字
- 定位到搜索按鈕
- 點(diǎn)擊搜索按鈕
- 定位結(jié)果列表
- 獲取結(jié)果并返回
以關(guān)鍵字驅(qū)動(dòng)的思想,即將這6步抽象出一個(gè)方法jd_search(),測(cè)試人員只需要寫(xiě)一句話(huà)就能完成以上所有動(dòng)作獲得結(jié)果。如:
result = jd_search('電腦')
方便、省時(shí)省力,測(cè)試人員可聚焦于產(chǎn)品業(yè)務(wù),而不是自動(dòng)化腳本和語(yǔ)言學(xué)習(xí)。
甚至可以直接在設(shè)計(jì)測(cè)試用例的時(shí)候?qū)戧P(guān)鍵字,由自動(dòng)化平臺(tái)去解析用例,都不需要寫(xiě)腳本。這方面最有名的自動(dòng)化框架就是RobotFrameWork。但是RobotFrameWork過(guò)于笨重。建議大家適當(dāng)抽象即可,不要過(guò)度抽象。
框架技術(shù)選擇
大多數(shù)框架采用java語(yǔ)言或是python語(yǔ)言來(lái)實(shí)現(xiàn),考慮到python容易掌握,各種庫(kù)也比較全,所以采用python語(yǔ)言來(lái)實(shí)現(xiàn)。
python自動(dòng)化框架最常用的有unittest和pytest,兩者都可以,這里采用python自帶的unittest。
對(duì)于WEB UI自動(dòng)化測(cè)試,沒(méi)有別的選擇,基本都是采用selenium來(lái)驅(qū)動(dòng)瀏覽器來(lái)完成。
對(duì)于接口自動(dòng)化測(cè)試,可采用的辦法較多,postman、jmeter都可以,但靈活性都不如直接采用python的request庫(kù)。
數(shù)據(jù)驅(qū)動(dòng),由于unittest沒(méi)有直接可用的dataprovider,采用常見(jiàn)的ddt來(lái)實(shí)現(xiàn)。
對(duì)于手機(jī)自動(dòng)化,暫未實(shí)現(xiàn),后續(xù)考慮加入,可采用appnium來(lái)實(shí)現(xiàn)。
測(cè)試數(shù)據(jù),第1階段采用excel管理,對(duì)于大型系統(tǒng),建議直接采用數(shù)據(jù)庫(kù)進(jìn)行管理。
所以總的來(lái)講,這個(gè)所謂的框架,就是東拼本湊,即沒(méi)有新思想,也沒(méi)有新技術(shù),只是將一些常用的技術(shù),按一定的思路組織起來(lái)、驅(qū)動(dòng)起來(lái)而已。
框架運(yùn)行結(jié)果
總共執(zhí)行6個(gè)用例,4個(gè)為京東搜索并抓取結(jié)果(WEB UI自動(dòng)化測(cè)試),2個(gè)為百度翻譯通用接口(接口自動(dòng)化測(cè)試)。
各用例對(duì)應(yīng)的定義方式(PO/DDT)
頁(yè)面定義方式
PO對(duì)象定義:京東主頁(yè)面定義了搜索框和搜索按鈕,以name為關(guān)鍵字,定義元素定位方式和執(zhí)行的動(dòng)作。
page_url = 'https://www.jd.com'
elements = [
{'name': 'search_ipt', 'desc': '搜索框', 'by': (By.ID, u'key'), 'action': 'send_keys()'},
{'name': 'search_btn', 'desc': '搜索按鈕', 'by': (By.CLASS_NAME, u'button'), 'action': 'click()'},
]
測(cè)試數(shù)據(jù)定義方式:
API接口定義方式
直接采用大家接口測(cè)試時(shí)熟悉的json格式來(lái)定義。
# 接口地址信息
uri_scheme = 'http'
endpoint = 'api.fanyi.baidu.com'
resource_path = '/api/trans/vip/translate'
url = uri_scheme + u'://' + endpoint + resource_path
# 保持不變的參數(shù)
_from = 'en'
_to = 'zh'
# 請(qǐng)求消息參數(shù)模板
req_param = {
"q": "", # 請(qǐng)求翻譯 query, UTF-8
"from": _from, # 翻譯源語(yǔ)言
"to": _to, # 翻譯目標(biāo)語(yǔ)言
"appid": "", # APP ID
"salt": "", # 隨機(jī)數(shù)
"sign": "", # 簽名,app_id+q+salt+密鑰 的MD5值
}
# 響應(yīng)消息參數(shù)模板
res_param = {
"from": _from,
"to": _to,
"trans_result": [
{
"src": "Hello World! This is 1st paragraph.",
"dst": "你好,世界!這是第一段。"
},
{
"src": "This is 2nd paragraph.",
"dst": "這是第二段。"
}
]
}
對(duì)應(yīng)的請(qǐng)求消息頭headers等內(nèi)容也可以定義在這里面。
主程序main.py
負(fù)責(zé)掃描用例,執(zhí)行用例,并生成測(cè)試報(bào)告,發(fā)送郵件。
??
測(cè)試執(zhí)行結(jié)果
3個(gè)腳本,每個(gè)腳本2條測(cè)試數(shù)據(jù),共6個(gè)用例。運(yùn)行main.py,執(zhí)行測(cè)試,測(cè)試結(jié)果如下,3個(gè)失敗的是故意修改了測(cè)試數(shù)據(jù)。
??
紅線(xiàn)部分為接口測(cè)試時(shí),自動(dòng)比對(duì)的json差異,預(yù)期結(jié)果為“蘋(píng)果”,實(shí)際結(jié)果為“期望值”。
測(cè)試報(bào)告郵件:
??
?測(cè)試報(bào)告詳情:
??
從零開(kāi)始搭建項(xiàng)目
一、開(kāi)發(fā)環(huán)境搭建
- 開(kāi)發(fā)IDE: pycharm?
- python: python 3?
- 依賴(lài)庫(kù):anaconda 3(個(gè)人比較懶,懶得一個(gè)一個(gè)庫(kù)的安裝,這個(gè)庫(kù)比較全)?
基本上都是直接上對(duì)應(yīng)官網(wǎng),下載安裝。準(zhǔn)備好了以后,直接開(kāi)干。
二、新建項(xiàng)目
pycharm上新建項(xiàng)目TestFrame,選擇好存放目錄,并在TestFrame項(xiàng)目下新建各模塊。注意除了Log和Report是新建Directory外,其它的都是新建Python Package,因?yàn)橄旅孢€要放py文件的。
pycharm上切換項(xiàng)目的python環(huán)境為anaconda,F(xiàn)ile—>Settings—>Project下面切換,如下圖:
??
三、基礎(chǔ)功能實(shí)現(xiàn)
1. 配置功能實(shí)現(xiàn)(Conf)
配置功能是項(xiàng)目的基礎(chǔ),所以先實(shí)現(xiàn)。在Conf目錄下新建2個(gè)文件,分別為config.ini和config.py。
config.ini內(nèi)容如下:
[sys]
base_url = https://www.jd.com
[smtp]
host = smtp.163.com
port = 465
user = example@163.com
passwd = password
暫時(shí)先加這么多,后續(xù)需要再慢慢添加。
config.py文件實(shí)現(xiàn)config.ini文件的讀取。
ini文件讀取,python有ConfigParser庫(kù)可以使用,那就直接用。
ConfigParser庫(kù)傳送門(mén)
但是每次取值都要用他的方法,比較麻煩,因此對(duì)它的方法進(jìn)行了一個(gè)繼承和改寫(xiě),直接將配置文件中所有內(nèi)容讀出來(lái)字典形式,方便后續(xù)使用。
代碼如下:
import os
from configparser import ConfigParser
# 使用相對(duì)目錄確定文件位置
_conf_dir = os.path.dirname(__file__)
_conf_file = os.path.join(_conf_dir, 'config.ini')
# 繼承ConfigParser,寫(xiě)一個(gè)將結(jié)果轉(zhuǎn)為dict的方法
class MyParser(ConfigParser):
def as_dict(self):
d = dict(self._sections)
for k in d:
d[k] = dict(d[k])
return d
# 讀取所有配置,以字典方式輸出結(jié)果
def _get_all_conf():
_config = MyParser()
result = {}
if os.path.isfile(_conf_file):
try:
_config.read(_conf_file, encoding='UTF-8')
result = _config.as_dict()
except OSError:
raise ValueError("Read config file failed: %s" % OSError)
return result
# 將各配置讀取出來(lái),放在變量中,后續(xù)其它文件直接引用這個(gè)這些變量
config = _get_all_conf()
sys_cfg = config['sys']
smtp_cfg = config['smtp']
print(sys_cfg)
print(smtp_cfg)
print(smtp_cfg['host'])
運(yùn)行結(jié)果:
{'base_url': 'https://www.jd.com'}
{'host': 'smtp.163.com', 'port': '465', 'user': 'example@163.com', 'passwd': 'password'}
smtp.163.com
后續(xù)其它文件就可以直接使用 sys_cfg 和 smtp_cfg 這兩個(gè)字典,以key的方式訪(fǎng)問(wèn)需要的配置內(nèi)容。
2. 日志功能實(shí)現(xiàn)(Log)
日志在項(xiàng)目中也是基礎(chǔ)功能,所以接著做日志。
python自帶logging庫(kù),可以定制日志的格式,就直接使用該庫(kù)實(shí)現(xiàn),沒(méi)必要自己造。
先去我們的配置文件中config.ini添加日志相關(guān)的配置,這里先定義3個(gè)配置:日志級(jí)別、日志格式、日志路徑。
[log]
log_level = logging.DEBUG
log_format = %(asctime)s - %(name)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s
log_path = Log
再在config.py中最后面添加一行代碼,把log相關(guān)的配置放在一個(gè)變量中,好直接使用。
log_cfg = config['log']
print(smtp_cfg)
打印出來(lái)看一下結(jié)果:
{'log_level': 'logging.DEBUG', 'log_format': '%(asctime)s - %(name)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s', 'log_path': 'Log'}
日志級(jí)別有:DEBUG、INFO、WARN、ERROR、FATAL。一般調(diào)試都是DEBUG,上線(xiàn)就改為INFO。
這里簡(jiǎn)單介紹一下日志格式log_format的內(nèi)容:
參數(shù) | 意義 | 說(shuō)明 |
---|---|---|
asctime | 時(shí)間 | 格式:2021-03-14 09:37:40,258 |
name | logger的名稱(chēng) | 簡(jiǎn)單理解就是將來(lái)把模塊名稱(chēng)填到這里,區(qū)分是誰(shuí)打的日志 |
filename | 文件名 | 哪個(gè)文件打印的這條日志 |
line | 行號(hào) | 哪一行打印的這條日志 |
levelname | 級(jí)別 | 日志的級(jí)別,注意是級(jí)別的name |
message | 內(nèi)容 | 我們打印的日志內(nèi)容 |
log_path | 日志文件 | 保存到哪個(gè)日志文件 |
再接著在Comm目錄下,新建一個(gè)Log.py,開(kāi)始定制日志。定制日志還有幾個(gè)問(wèn)題要提前考慮:
一是存放目錄問(wèn)題,我們這里使用了固定目錄,所以問(wèn)題不大。
二是日志分割、滾動(dòng)問(wèn)題,每天跑持續(xù)集成,大量用例生成大量日志,日志堆成山。如果覺(jué)得日志有用呢,就搞個(gè)ELK把日志取走存放起來(lái)做分析。如果覺(jué)得日志沒(méi)用呢,保存幾天后就刪除掉。無(wú)論怎么講,都要實(shí)現(xiàn)日志的分割和滾動(dòng)。
幸好你想到的大佬們?cè)缇拖氲搅耍琹ogging模塊就有這個(gè)功能,只要配置一下就可以了。
下面開(kāi)搞,引入logging庫(kù),把項(xiàng)目的根路徑取出來(lái),把上面config.ini中的日志配置取過(guò)來(lái),最后拼接好日志文件存放的絕對(duì)路徑:
import os
import logging
from Conf.Config import log_cfg
_BaseHome = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
_log_level = eval(log_cfg['log_level'])
_log_path = log_cfg['log_path']
_log_format = log_cfg['log_format']
_log_file = os.path.join(_BaseHome, _log_path, 'log.txt')
注意上面log_level的寫(xiě)法,這里用了個(gè)eval,如果不加這個(gè)函數(shù),log_level取過(guò)來(lái)是個(gè)字符串,沒(méi)法直接用,通過(guò)eval執(zhí)行后,就變成了logging定義的對(duì)象了。
再配置日志,引入TimedRotatingFileHandler這個(gè)東東,這是實(shí)現(xiàn)滾動(dòng)日志的。
from logging.handlers import TimedRotatingFileHandler
def log_init():
logger = logging.getLogger('main')
logger.setLevel(level=_log_level)
formatter = logging.Formatter(_log_format)
handler = TimedRotatingFileHandler(filename=_log_file, when="D", interval=1, backupCount=7)
handler.setLevel(_log_level)
handler.setFormatter(formatter)
logger.addHandler(handler)
console = logging.StreamHandler()
console.setLevel(_log_level)
console.setFormatter(formatter)
logger.addHandler(console)
這個(gè)日志里面,加了兩個(gè)輸出,handler用于向日志文件打印日志,console 用于向終端打印日志,兩個(gè)的定義方式不同。
TimedRotatingFileHandler的參數(shù)簡(jiǎn)介:
參數(shù) | 意義 | 說(shuō)明 |
---|---|---|
filename | 日志文件 | 沒(méi)啥好說(shuō)的 |
when | 切割條件 | 按周(W)、天(D)、時(shí)(H)、分(M)、秒(S)切割 |
interval | 間隔 | 就是幾個(gè)when切割一次。when是W,interval是3的話(huà)就代表3周切割一次 |
backupCount | 日志備份數(shù)量 | 就是保留幾個(gè)日志文件,起過(guò)這個(gè)數(shù)量,就把最早的刪除掉,從而滾動(dòng)刪除 |
我這里配置的是每天生成1個(gè)日志文件,保留7天的日志。
日志就做好了,試一下效果。
log_init()
logger = logging.getLogger('main')
logger.info('log test----------')
運(yùn)行結(jié)果:
2021-03-15 21:53:41,972 - main - Log.py[line:49] - INFO - log test----------
其它文件使用日志:
先在main.py里面引入這個(gè)log_init(),在最開(kāi)始的時(shí)候初始化一下,日志就配置好了。
再在各個(gè)要使用日志的文件中,直接按下面這種方式使用:
import logging
logger = logging.getLogger('main.jd')
注意各個(gè)模塊自己getLogger的時(shí)候,直接main后面加上“.模塊名”,就能使用同一個(gè)logger區(qū)分模塊了。
到這里日志功能就完成了。
順手做個(gè)截圖的功能,供大家使用。截圖可以直接在用例里面用selenium提供的截圖功能,也可以自己做一個(gè)公共的。下面是用PIL里面的功能做的截圖。
from PIL import ImageGrab
# 先定義截圖文件的存放路徑,這里在Log目錄下建個(gè)Screen目錄,按天存放截圖
_today = time.strftime("%Y%m%d")
_screen_path = os.path.join(_BaseHome, _log_path, 'Screen', _today)
#再使用PIL的ImageGrab實(shí)現(xiàn)截圖
def screen(name):
t = time.time()
png = ImageGrab.grab()
if not os.path.exists(_screen_path):
os.makedirs(_screen_path)
image_name = os.path.join(_screen_path, name)
png.save('%s_%s.png' % (image_name, str(round(t * 1000)))) # 文件名后面加了個(gè)時(shí)間戳,避免重名
運(yùn)行這個(gè)方法就能截圖了,大功告成。截圖文件其實(shí)也需要一個(gè)滾動(dòng)刪除,后面有時(shí)間再寫(xiě)吧。
3. 讀取EXCEL實(shí)現(xiàn)(data)
接著寫(xiě)一個(gè)讀取EXCEL文件數(shù)據(jù)的功能吧,這個(gè)項(xiàng)目里面主要是用來(lái)讀測(cè)試數(shù)據(jù),以實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)。
python讀取excel數(shù)據(jù),我看大家都喜歡用xlrd和xlwt,還有用openpyxl的,對(duì)于我這種懶人來(lái)講,都太麻煩了。
我們用pandas來(lái)干,一句話(huà)的事情,搞那么多干嗎,用python就是要快。
在Comm目錄下,新建一個(gè)data.py,專(zhuān)門(mén)來(lái)處理數(shù)據(jù)。引入pandas,直接用pandas的read_excel讀excel,而且支持它原始的其它參數(shù),只是最后將結(jié)果轉(zhuǎn)了字典,方便使用:
import pandas as pd
def read_excel(file, **kwargs):
data_dict = []
try:
data = pd.read_excel(file, **kwargs)
data_dict = data.to_dict('records')
finally:
return data_dict
隨便放一個(gè)excel在同一個(gè)目錄下,填上數(shù)據(jù),試一下效果。excel里面2頁(yè)數(shù)據(jù),Sheet1如下:
??
Sheet2如下:
?
調(diào)用我們寫(xiě)好的方法,打印數(shù)據(jù):
sheet1 = read_excel('baidu_fanyi.xlsx')
sheet2 = read_excel('baidu_fanyi.xlsx', sheet_name='Sheet2')
print(sheet1)
print(sheet2)
運(yùn)行結(jié)果如下:
[{'req.q': '計(jì)算機(jī)\n計(jì)算機(jī)', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en', 'res.trans_result.0.src': '計(jì)算機(jī)', 'res.trans_result.0.dst': 'computer', 'res.trans_result.1.src': '計(jì)算機(jī)', 'res.trans_result.1.dst': 'computer'},
{'req.q': 'computer\nexpected value', 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh', 'res.trans_result.0.src': 'computer', 'res.trans_result.0.dst': '計(jì)算機(jī)', 'res.trans_result.1.src': 'expected value', 'res.trans_result.1.dst': '蘋(píng)果'}]
[{'req.q': '計(jì)算機(jī)', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en'},
{'req.q': 'computer', 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh'}]
每頁(yè)數(shù)據(jù)都讀出來(lái)了,而且每一行都是字典形式,直接通過(guò)key就可以方便的使用。
pandas還能直接計(jì)算數(shù)據(jù),如通過(guò)幾個(gè)列算加密簽名,寫(xiě)動(dòng)態(tài)cookie等,使用方法也很簡(jiǎn)單。比如在數(shù)據(jù)中增加一列sign, 讓它簡(jiǎn)單等于 req.from列 + ‘.aaaa.’ + req.to列,給大家演示一下。
data = pd.read_excel('baidu_fanyi.xlsx')
data['sign'] = data["req.from"] +'.aaaaa.' + data["req.to"]
data_dict = data.to_dict('records')
print(data_dict)
運(yùn)行結(jié)果:
[{'req.q': '計(jì)算機(jī)\n計(jì)算機(jī)', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en', 'res.trans_result.0.src': '計(jì)算機(jī)', 'res.trans_result.0.dst': 'computer', 'res.trans_result.1.src': '計(jì)算機(jī)', 'res.trans_result.1.dst': 'computer', 'sign': 'zh.aaaaa.en'},
{'req.q': 'computer\nexpected value', 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh', 'res.trans_result.0.src': 'computer', 'res.trans_result.0.dst': '計(jì)算機(jī)', 'res.trans_result.1.src': 'expected value', 'res.trans_result.1.dst': '蘋(píng)果', 'sign': 'en.aaaaa.zh'}]
我們可以看到多了一列sign,值就是自動(dòng)根據(jù)每一行的數(shù)據(jù)算出來(lái)的,這對(duì)于我們數(shù)據(jù)驅(qū)動(dòng)來(lái)講,去計(jì)算一些動(dòng)態(tài)值非常有用。我這里沒(méi)有用到動(dòng)態(tài)的,只是讀而已。大家如果要計(jì)算,就要自己寫(xiě)計(jì)算方法。
pandas還支持直接讀各種主流數(shù)據(jù)庫(kù),后面擴(kuò)展也很方便,我們一直都用它。
4. 郵件發(fā)送實(shí)現(xiàn)(Email)
實(shí)現(xiàn)郵件功能,用于發(fā)送測(cè)試報(bào)告。使用python的smtplib模塊實(shí)現(xiàn)。
先在Conf目錄下的config.ini中添加好郵件相關(guān)的配置:
[smtp]
host = smtp.163.com
port = 465
user = example@163.com
passwd = password
[email]
sender = example@163.com
receivers = example@qq.com, example@163.com
再在Config.py中將它們?nèi)〉阶兞恐蟹藕茫?/p>
smtp_cfg = config['smtp']
email_cfg = config['email']
然后在Comm目錄下新建Email.py,開(kāi)始擼代碼。郵件支持了定義主題、正文和多個(gè)附件,控制了單個(gè)附件大小和附件總數(shù)。代碼如下:
import smtplib
import os
import logging
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.header import Header
from Conf.Config import smtp_cfg, email_cfg
_FILESIZE = 20 # 單位M, 單個(gè)附件大小
_FILECOUNT = 10 # 附件個(gè)數(shù)
_smtp_cfg = smtp_cfg
_email_cfg = email_cfg
_logger = logging.getLogger('main.email')
class Email:
def __init__(self, subject, context=None, attachment=None):
self.subject = subject
self.context = context
self.attachment = attachment
self.message = MIMEMultipart()
self._message_init()
def _message_init(self):
if self.subject:
self.message['subject'] = Header(self.subject, 'utf-8') # 郵件標(biāo)題
else:
raise ValueError("Invalid subject")
self.message['from'] = _email_cfg['sender'] # from
self.message['to'] = _email_cfg['receivers'] # to
if self.context:
self.message.attach(MIMEText(self.context, 'html', 'utf-8')) # 郵件正文內(nèi)容
# 郵件附件
if self.attachment:
if isinstance(self.attachment, str):
self._attach(self.attachment)
if isinstance(self.attachment, list):
count = 0
for each in self.attachment:
if count <= _FILECOUNT:
self._attach(each)
count += 1
else:
_logger.warning('Attachments is more than ', _FILECOUNT)
break
def _attach(self, file):
if os.path.isfile(file) and os.path.getsize(file) <= _FILESIZE * 1024 * 1024:
attach = MIMEApplication(open(file, 'rb').read())
attach.add_header('Content-Disposition', 'attachment', filename=os.path.basename(file))
attach["Content-Type"] = 'application/octet-stream'
self.message.attach(attach)
else:
_logger.error('The attachment is not exist or more than %sM: %s' % (_FILESIZE, file))
def send_mail(self):
s = smtplib.SMTP_SSL(_smtp_cfg['host'], int(_smtp_cfg['port']))
result = True
try:
s.login(self._smtp_cfg['user'], self._smtp_cfg['passwd'])
s.sendmail(self._smtp_cfg['sender'], self._smtp_cfg['receivers'], self.message.as_string())
except smtplib.SMTPException as e:
result = False
_logger.error('Send mail failed', exc_info=True)
finally:
s.close()
return result
郵件初始化發(fā)送時(shí)的調(diào)用方式如下:
mail = Email(title, context, file)
send = mail.send_mail()
print(send)
返回結(jié)果為T(mén)rue則發(fā)送成功,否則發(fā)送失敗。
四、WEB UI自動(dòng)化
WEB UI自動(dòng)化,采用 selenium來(lái)完成。通過(guò)PO對(duì)象、測(cè)試數(shù)據(jù)、業(yè)務(wù)邏輯三者分離的方式來(lái)實(shí)現(xiàn)。
另外一個(gè)主旨是盡量讓測(cè)試人員使用selenium原生的各種方法,而不要做過(guò)多封裝。原因很簡(jiǎn)單,不要讓測(cè)試人員來(lái)學(xué)這個(gè)框架,而是去學(xué)selenium,這樣以后他出去換工作才有飯吃。如果過(guò)度封裝,就會(huì)讓測(cè)試人員來(lái)學(xué)這個(gè)框架,他以后出去selenium都不會(huì)用,這不是害了別人么。框架的目的只是把對(duì)象、數(shù)據(jù)、業(yè)務(wù)邏輯三者驅(qū)動(dòng)起來(lái),讓測(cè)試人員工作起來(lái)更快。
我們以京東搜索爬蟲(chóng)為例來(lái)看如何構(gòu)建這三者的關(guān)系:在京東主頁(yè)面,搜索“電腦”,再獲取搜索結(jié)果,保存。
1. 頁(yè)面PO對(duì)象配置
打開(kāi)京東商城主頁(yè),找到搜索框元素、和搜索按鈕元素,分別確定他們的定位方式,以及元素對(duì)應(yīng)的操作。
然后建立這個(gè)頁(yè)面對(duì)象,在Page下新建一個(gè)名為"jd"的python package,再在這個(gè)package下新建一個(gè)jd.py,用來(lái)定義京東商城的主頁(yè)面對(duì)象。
from selenium.webdriver.common.by import By
page_url = 'https://www.jd.com'
elements = [
{'name': 'search_ipt', 'desc': '搜索框點(diǎn)擊', 'by': (By.ID, u'key'), 'ec': 'presence_of_element_located', 'action': 'send_keys()'},
{'name': 'search_btn', 'desc': '搜索按鈕點(diǎn)擊', 'by': (By.CLASS_NAME, u'button'), 'ec': 'presence_of_element_located', 'action': 'click()'},
]
name: 每個(gè)元素+操作的唯一標(biāo)識(shí)。一個(gè)元素可能由于操作不同,而要定義多個(gè),但大部分只要定義一個(gè)。
desc:元素+操作的描述。
by:元素的定位方式,使用selenium的原生定位方式,不自己定義封裝。
ec: 等待元素出現(xiàn)的方式,這個(gè)暫時(shí)未用。
action:元素的對(duì)應(yīng)操作。使用原生的selenium動(dòng)作方法,不自己定義封裝。
京東商城主頁(yè)面現(xiàn)在只用到這兩個(gè),就只定義這兩個(gè)。
搜索結(jié)果頁(yè)面,定義如下:
from selenium.webdriver.common.by import By
page_url = 'https://search.jd.com/'
elements = [
{'name': 'result_list', 'desc': '結(jié)果列表', 'by': (By.CLASS_NAME, u'gl-item'), 'ec': 'presence_of_all_elements_located', 'action': None},
{'name': 'price', 'desc': '價(jià)格', 'by': (By.XPATH, u".//div[@class='p-price']/strong/i"), 'ec': 'presence_of_element_located', 'action': 'text'},
{'name': 'pname', 'desc': '描述', 'by': (By.XPATH, u".//div[@class='p-name p-name-type-2']/a/em"), 'ec': 'presence_of_element_located', 'action': 'text'}
]
2. 實(shí)現(xiàn)basePage基類(lèi)
basePage基類(lèi)的實(shí)現(xiàn)思想是不做過(guò)多的封裝,盡量讓測(cè)試人員直接使用selenium原裝的方法,而不像其它框架一樣什么都封裝在這里面。
所以我對(duì)basePage的定義是:根據(jù)業(yè)務(wù)邏輯(測(cè)試用例)指定的元素,輸入的數(shù)據(jù),協(xié)助它完成元素定位和操作,僅此而已。
當(dāng)然如果去封裝各種東西也是可以的,直接在里面加就行了。
在Page目錄下,新建basePage.py,開(kāi)始擼代碼:
from selenium.webdriver.common.by import By
from selenium import webdriver
import os
import importlib
import logging
SimpleActions = ['clear()', 'send_keys()', 'click()', 'submit()', 'size', 'text', 'is_displayed()', 'get_attribute()']
logger = logging.getLogger('main.page')
class Page(object):
def __init__(self, driver, page):
self.driver = driver
self.page = page
self.elements = get_page_elements(page)
self.by = ()
self.action = None
def _get_page_elem(self, elem):
# 獲取定位元素的 by,以及操作action
for each in self.elements:
if each['name'] == elem:
self.by = each['by']
if 'action' in each and each['action'] is not None:
self.action = each['action']
else:
self.action = None
def oper_elem(self, elem, args=None):
self._get_page_elem(elem)
cmd = self._selenium_cmd('find_element', args)
return eval(cmd)
def oper_elems(self, elem, args=None):
self._get_page_elem(elem)
cmd = self._selenium_cmd('find_elements', args)
return eval(cmd)
def _selenium_cmd(self, find_type='find_element', args=None):
# 拼接 selenium 查找命令, 查找單個(gè)元素時(shí)find_type為'find_element',多個(gè)元素時(shí)為'find_elements'
cmd = 'self.driver.' + find_type + '(*self.by)'
if self.action:
if self.action in SimpleActions:
cmd = cmd + '.' + self.action
if args:
cmd = cmd[:-1] + 'args' + ')'
return cmd
def get_page_elements(page):
"""動(dòng)態(tài)加載頁(yè)面定義文件,獲取文件中定義的元素列表elements"""
elements = None
if page:
try:
m = importlib.import_module(page)
elements = m.elements
except Exception as e:
logger.error('error info : %s' %(e))
return elements
?這里面主要的只包含3個(gè)方法,一個(gè)是動(dòng)態(tài)加載指定的PO對(duì)象獲取元素列表,一個(gè)是在獲取的元素列表中去找到當(dāng)前要操作的元素,最后一個(gè)就是拼接原生的selenium命令,將測(cè)試數(shù)據(jù)插入到動(dòng)作里面去。
其它的就簡(jiǎn)單了,直接調(diào)用selenium運(yùn)行拼接出來(lái)的命令,把結(jié)果返回出去。
這里要注意的是,有些復(fù)雜的selenium操作,不能這么簡(jiǎn)單的拼命令,要特殊處理,這里暫時(shí)沒(méi)弄;簡(jiǎn)單的命令,也沒(méi)有列全。后面再慢慢加。
3. 寫(xiě)業(yè)務(wù)測(cè)試用例
下面開(kāi)始寫(xiě)測(cè)試用例。?
在Testcase目錄下,新建一個(gè)python package:Model1。在Model1下面再建一個(gè)目錄:Testdata,用于放測(cè)試數(shù)據(jù);建一個(gè)python package:Case,用于放用例腳本。目錄結(jié)構(gòu)如下:?
準(zhǔn)備測(cè)試數(shù)據(jù):
準(zhǔn)備一份excel數(shù)據(jù)(test_jd_desktop.xlsx),存放在Model1/Testdata/jd下:
keyword:搜索的關(guān)鍵字
count:搜索結(jié)果總數(shù),只抓了一頁(yè),應(yīng)該是60個(gè)
實(shí)現(xiàn)業(yè)務(wù)用例:
在Model1/Case/jd下新建一個(gè)文件:test_jd_desktop.py,開(kāi)始寫(xiě)用例腳本。
用例使用unittest結(jié)合DDT來(lái)實(shí)現(xiàn),具體代碼如下:
import os
import unittest
import ddt
import logging
from selenium import webdriver
from time import sleep
from Page.basePage import Page
from Comm.Log import screen
from Comm.data import read_excel
from main import TestCasePath
logger = logging.getLogger('main.jd')
# 讀取測(cè)試數(shù)據(jù)
file = os.path.join(TestCasePath, 'Model1/Testdata/jd/test_jd_desktop.xlsx')
test_data = read_excel(file)
PO_jd = 'Page.jd.jd'
PO_search = 'Page.jd.search_jd'
@ddt.ddt # 數(shù)據(jù)驅(qū)動(dòng)
class TestJdSearchDesktop(unittest.TestCase):
"""京東搜索測(cè)試"""
def setUp(self):
self.driver = webdriver.Chrome()
self.count = 0
self.result = []
@ddt.data(*test_data) # 數(shù)據(jù)驅(qū)動(dòng)傳具體數(shù)據(jù)
def testJdSearchDesktop(self, test_data):
"""京東搜索測(cè)試--電腦"""
url = 'https://www.jd.com'
keyword = test_data['keyword']
wait = self.driver.implicitly_wait(5)
try:
self.driver.get(url)
# 實(shí)例化jd主頁(yè)面
jd = Page(self.driver, PO_jd)
# 實(shí)例化jd搜索結(jié)果頁(yè)面
jd_search = Page(self.driver, PO_search)
wait
# jd主頁(yè)面的搜索框元素中輸入關(guān)鍵字
jd.oper_elem('search_ipt', keyword)
wait
# 操作jd主頁(yè)面的搜索按鈕元素
jd.oper_elem('search_btn')
sleep(1)
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
sleep(1)
# jd搜索結(jié)果頁(yè)面,獲取結(jié)果列表
lis = jd_search.oper_elems('result_list')
# 在取到的結(jié)果列表中,循環(huán)獲取商品價(jià)格和商品名稱(chēng),結(jié)果存EXCEL就沒(méi)寫(xiě)了
for each in lis:
self.count += 1
page_each = Page(each, PO_search)
price = page_each.oper_elem('price')
name = page_each.oper_elem('pname')
self.result.append([name, price])
sleep(1)
except Exception as E:
logger.error('error info : %s' % (E))
screen(test_data['keyword'])
# 判斷是不是取到了60個(gè)商品
self.assertEqual(test_data['count'], self.count)
def tearDown(self):
self.driver.quit()
五、實(shí)現(xiàn)主程序
主程序的主要作用是 組織用例,執(zhí)行用例,生成報(bào)告,發(fā)送測(cè)試報(bào)告郵件。
組織用例和執(zhí)行用例都直接用unittest;
生成報(bào)告,采用BeautifulReport;
下面開(kāi)始擼main.py的代碼:
import unittest
import os
import time
import logging
from Comm.Email import Email
from Comm.Log import log_init
from BeautifulReport import BeautifulReport
# 定義各目錄
ProjectHome = os.path.split(os.path.realpath(__file__))[0]
PageObjectPath = os.path.join(ProjectHome, "Page")
TestCasePath = os.path.join(ProjectHome, "Testcase")
ReportPath = os.path.join(ProjectHome, "Report")
#對(duì)測(cè)試結(jié)果關(guān)鍵信息進(jìn)行匯總,做為郵件正文
def summary_format(result):
summary = "\n" + u"<p> 測(cè)試結(jié)果匯總信息 </p>" + "\n" + \
u"<p> 開(kāi)始時(shí)間: " + result['beginTime'] + u" </p>" + "\n" + \
u"<p> 運(yùn)行時(shí)間: " + result['totalTime'] + u" </p>" + "\n" + \
u"<p> 執(zhí)行用例數(shù): " + str(result['testAll']) + u" </p>" + "\n" + \
u"<p> 通過(guò)用例數(shù): " + str(result['testPass']) + u" </p>" + "\n" + \
u"<p> 失敗用例數(shù): " + str(result['testFail']) + u" </p>" + "\n" + \
u"<p> 忽略用例數(shù): " + str(result['testSkip']) + u" </p>" + "\n"
return summary
# 發(fā)送郵件
def send_email(file, context):
title = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + '自動(dòng)化測(cè)試結(jié)果'
mail = Email(title, context, file)
send = mail.send_mail()
if send:
print('測(cè)試報(bào)告郵件發(fā)送成功')
else:
print('測(cè)試報(bào)告郵件發(fā)送失敗')
# 加載測(cè)試用例
def get_suite(case_path=TestCasePath, rule="test_*.py"):
"""加載所有的測(cè)試用例"""
unittest_suite = unittest.TestSuite()
discover = unittest.defaultTestLoader.discover(case_path, pattern=rule, top_level_dir=None)
for each in discover:
unittest_suite.addTests(each)
return unittest_suite
# 執(zhí)行用例,生成測(cè)試報(bào)告,并返回報(bào)告附件路徑、郵件正文內(nèi)容
def suite_run(unittest_suite):
"""執(zhí)行所有的用例, 并把結(jié)果寫(xiě)入測(cè)試報(bào)告"""
run_result = BeautifulReport(unittest_suite)
now = time.strftime("%Y%m%d%H%M%S", time.localtime())
filename = now + '_report.html'
run_result.report(filename=filename, description=now, report_dir=ReportPath)
rpt_summary = summary_format(run_result.fields)
return os.path.join(ReportPath, filename), rpt_summary
# 主程序,加載用例,執(zhí)行用例,發(fā)送郵件
if __name__ == "__main__":
suite = get_suite()
report_file, report_summary = suite_run(suite)
print(report_summary)
send_email(report_file, report_summary)
API自動(dòng)化,采用 request庫(kù)來(lái)完成。還是通過(guò)PO對(duì)象、測(cè)試數(shù)據(jù)、業(yè)務(wù)邏輯三者分離的方式來(lái)實(shí)現(xiàn)。
這里以百度通用翻譯接口為例,這個(gè)接口對(duì)個(gè)人用戶(hù)是免費(fèi)的,大家可以自己去申請(qǐng)。
1. API對(duì)象配置
在APIs下面新建python package:fanyi,再在fanyi下面建baidu.py。
將百度通用翻譯接口定義在這里面,直接采用大家熟悉的json格式:
"""百度通用翻譯接口"""
API_NAME = 'fanyi'
# 地址信息
uri_scheme = 'http'
endpoint = 'api.fanyi.baidu.com'
resource_path = '/api/trans/vip/translate'
url = uri_scheme + u'://' + endpoint + resource_path
# 保持不變的參數(shù)
_from = 'en'
_to = 'zh'
# 請(qǐng)求消息參數(shù)
req_param = {
"q": "", # 請(qǐng)求翻譯 query, UTF-8
"from": _from, # 翻譯源語(yǔ)言
"to": _to, # 翻譯目標(biāo)語(yǔ)言
"appid": "", # APP ID
"salt": "", # 隨機(jī)數(shù)
"sign": "", # 簽名,appid+q+salt+密鑰 的MD5值
}
# 響應(yīng)消息參數(shù)
res_param = {
"from": _from,
"to": _to,
"trans_result": [
{
"src": "Hello World! This is 1st paragraph.",
"dst": "你好,世界!這是第一段。"
},
{
"src": "This is 2nd paragraph.",
"dst": "這是第二段。"
}
]
}
2.實(shí)現(xiàn)base_api基類(lèi)
base_api基類(lèi),主要是將數(shù)據(jù)、API對(duì)象、測(cè)試用例三者連起來(lái);
在APIs目錄下,新建base_api.py,代碼如下:
import logging
import random
import importlib
import copy
import json
import unittest
from hashlib import md5
from ipaddress import ip_address
from Comm.compare import json_compare
logger = logging.getLogger('main.api')
req_prefix = 'req.'
res_prefix = 'res.'
def _separate_data(data, prefix='req.'):
pfx = prefix
result = {}
for key, value in data.items():
if key.startswith(pfx):
req_key = key[len(pfx):]
result[req_key] = value
return result
def _get_cmd(key, dict_name='payload'):
separator = '.'
cmd = dict_name
if separator in key:
data_key = key.split(separator)
for each in data_key:
if each.isdigit():
cmd = cmd + '[' + each + ']'
else:
cmd = cmd + '[\'' + each + '\']'
cmd = cmd + ' = value'
else:
cmd = cmd + '[key] = value'
return cmd
def check_result(unittest_testcase, x, y):
# 只有x,y完全相同才能通過(guò),任意不同則返回失敗。建議自己在用例中做結(jié)果檢查
testcase = unittest_testcase
diff = json_compare(x, y)
testcase.assertEqual(x, y)
class BaseAPI(object):
def __init__(self, api):
self.api = api
self.api_name = None
self.url = ''
self.req_template = {}
self.res_template = {}
self._get_api_param()
def _get_api_param(self):
"""動(dòng)態(tài)加載API定義文件,獲取文件中定義的API參數(shù)"""
try:
m = importlib.import_module(self.api)
self.api_name = m.API_NAME
self.url = m.url
self.req_template = m.req_param
self.res_template = m.res_param
except Exception as e:
logger.error('error info : %s' % e)
def payload(self, data=None):
payload = copy.deepcopy(self.req_template)
if data:
req_pre = '.'.join([self.api_name, req_prefix])
req_data = _separate_data(data, req_pre)
for key, value in req_data.items():
cmd = _get_cmd(key, 'payload')
exec(cmd)
return payload
def load_expected(self, data=None):
expected = copy.deepcopy(self.res_template)
if data:
res_pre = '.'.join([self.api_name, res_prefix])
res_data = _separate_data(data, res_pre)
for key, value in res_data.items():
cmd = _get_cmd(key, 'expected')
exec(cmd)
return expected
這里面的思路是:
- 動(dòng)態(tài)加載API對(duì)象,獲取API請(qǐng)求參數(shù)模板、和響應(yīng)參數(shù)模板;
- payload的時(shí)候,從測(cè)試數(shù)據(jù)中,取出API請(qǐng)求相關(guān)的數(shù)據(jù)(以API名.req開(kāi)頭,如fanyi.req.q),填入模板,沒(méi)有的就用模板數(shù)據(jù);
- 加載預(yù)期結(jié)果的時(shí)候,從測(cè)試數(shù)據(jù)中,取出API響應(yīng)相關(guān)的數(shù)據(jù)(以API名.res開(kāi)頭,如fanyi.res.trans_result.0.src),填入模板,沒(méi)有的就用模板數(shù)據(jù)。
- 提供json比較的方法;
- 提供了一個(gè)隨機(jī)handers。
具體的大家看一下就明白了。想進(jìn)一步封裝的還可以繼續(xù)封裝,比如生成hearders,數(shù)據(jù)配完了直接發(fā)送,取到結(jié)果直接比對(duì)什么的。但是建議不要過(guò)度封裝。
附j(luò)son比較的方法:
import json_tools
def json_compare(x, y):
diff = json_tools.diff(x, y)
if diff:
for action in diff:
if 'add' in action:
print('++增加元素:', action['add'], ' 值:', action['value'])
elif 'remove' in action:
print('--刪除元素:', action['remove'], ' 值:', action['prev'])
elif 'replace' in action:
print('**修改元素:', action['replace'], ' 值:', action['prev'], '-->', action['value'])
return diff
3.測(cè)試用例
在Testcase下建API模塊,API模塊下建Case和Testdata,分別放用例和數(shù)據(jù),目錄如下:??
定義測(cè)試數(shù)據(jù):
測(cè)試數(shù)據(jù)需要按一定的格式處理,即每個(gè)參數(shù)以api名稱(chēng)開(kāi)頭,用“.”連接,然后用res和req來(lái)區(qū)分響應(yīng)還是請(qǐng)求,后面就是具體的參數(shù)了,多級(jí)參數(shù)以“.”連接。具體如下:
測(cè)試用例腳本:
仍然用unittest和ddt來(lái)實(shí)現(xiàn)。
import os
import unittest
import ddt
import random
import json
import requests
from time import sleep
from Comm.data import read_excel
from Comm.encryption import make_md5
from main import TestCasePath
from APIs.base_api import BaseAPI, check_result
# 開(kāi)通普通個(gè)人的百度翻譯接口,設(shè)置appid和appkey.
app_id = your appid
app_key = your appkey
# 獲取測(cè)試數(shù)據(jù)
file = os.path.join(TestCasePath, 'API/TestData/baidu_fanyi.xlsx')
test_data = read_excel(file)
api = 'APIs.fanyi.baidu'
@ddt.ddt
class TestBaiduFanyi(unittest.TestCase):
"""百度翻譯接口測(cè)試"""
def setUp(self):
self.api = BaseAPI(api)
@ddt.data(*test_data)
def test_baidu_fanyi(self, test_data):
"""百度翻譯接口測(cè)試"""
api = self.api
# Build test_data,這是些動(dòng)態(tài)參數(shù),在這里計(jì)算
test_data['fanyi.req.appid'] = app_id
salt = random.randint(32768, 65536)
test_data['fanyi.req.salt'] = salt
sign = make_md5(app_id + test_data['fanyi.req.q'] + str(salt) + app_key)
test_data['fanyi.req.sign'] = sign
# Build request
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = api.payload(test_data )
# Send request
r = requests.post(api.url, params=payload, headers=headers)
result = r.json()
expected = api.load_expected(test_data)
self.assertEqual(r.status_code, 200)
check_result(self, expected, result) # 簡(jiǎn)單的模板驗(yàn)證,大家最好自己寫(xiě)驗(yàn)證。
sleep(0.5)
然后運(yùn)行主程序,API自動(dòng)化測(cè)試也就可以跑起來(lái)了。
補(bǔ):MD5函數(shù)
from hashlib import md5
def make_md5(s, encoding='utf-8'):
return md5(s.encode(encoding)).hexdigest()
結(jié)語(yǔ)
這篇貼子到這里就結(jié)束了,最后,希望看這篇帖子的朋友能夠有所收獲。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-757618.html
如果你覺(jué)得文章還不錯(cuò),請(qǐng)大家?點(diǎn)贊、分享、留言?下,因?yàn)檫@將是我持續(xù)輸出更多優(yōu)質(zhì)文章的最強(qiáng)動(dòng)力!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-757618.html
到了這里,關(guān)于從零搭建完整python自動(dòng)化測(cè)試框架(UI自動(dòng)化和接口自動(dòng)化)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!