前段時間由于公司測試方向的轉(zhuǎn)型,由原來的web頁面功能測試轉(zhuǎn)變成接口測試,之前大多都是手工進行,利用postman和jmeter進行的接口測試,后來,組內(nèi)有人講原先web自動化的測試框架移駕,搭建成接口的自動化框架,使用的是java語言。對于一個不會Java的小伙伴,怎樣完成自動化測試呢?
今天,就和大家分享一下我自己用Python寫的接口自動化測試框架吧,沒有Java基礎(chǔ)的小伙伴也能快速上手哦。
如果有想要學(xué)習(xí)Python或者正在學(xué)習(xí)Python中的小伙伴,需要學(xué)習(xí)資料的話,可以到我的微信公眾號:Python學(xué)習(xí)知識圈,后臺回復(fù):“01”,即可拿Python學(xué)習(xí)資料1、構(gòu)建思路
正常的接口測試流程是什么?
腦海里的反應(yīng)是不是這樣的:確定測試接口的工具 —> 配置需要的接口參數(shù) —> 進行測試 —> 檢查測試結(jié)果(有的需要數(shù)據(jù)庫輔助) —> 生成測試報告(html報告)
根據(jù)這個過程,我們一步步來搭建框架。在這個過程中,我們需要做到業(yè)務(wù)和數(shù)據(jù)的分離,這樣才能靈活,達到我們寫框架的目的。只要好好做,一定可以成功。這也是我當(dāng)初對自己說的。
2、結(jié)構(gòu)劃分
我的結(jié)構(gòu)是這樣的,大家可以參考下:

common:存放一些共通的方法
result:執(zhí)行過程中生成的文件夾,里面存放每次測試的結(jié)果
testCase:用于存放具體的測試case
testFile:存放測試過程中用到的文件,包括上傳的文件,測試用例以及 數(shù)據(jù)庫的sql語句
caselist:txt文件,配置每次執(zhí)行的case名稱
config:配置一些常量,例如數(shù)據(jù)庫的相關(guān)信息,接口的相關(guān)信息等
readConfig: 用于讀取config配置文件中的內(nèi)容
runAll:用于執(zhí)行case既然整體結(jié)構(gòu)有了劃分,接下來就該一步步的填充整個框架了,首先,我們先來看看config.ini和readConfig.py兩個文件,從他們?nèi)胧?,個人覺得比較容易走下去噠。
3、配置文件
我們來看下文件的內(nèi)容是什么樣子的:
[DATABASE]
host = 50.23.190.57
username = xxxxxx
password = ******
port = 3306
database = databasename
[HTTP]
# 接口的url
baseurl = http://xx.xxxx.xx
port = 8080
timeout = 1.0
[EMAIL]
mail_host = smtp.163.com
mail_user = xxx@163.com
mail_pass = *********
mail_port = 25
sender = xxx@163.com
receiver = xxxx@qq.com/xxxx@qq.com
subject = python
content = "All interface test has been complited\nplease read the report file about the detile of result in the attachment."
testuser = Someone
on_off = 1
相信大家都知道這樣的配置文件,沒錯,所有一成不變的東西,我們都可以放到這里來。哈哈,怎么樣,不錯吧。
4、運用get方法讀取配置文件
現(xiàn)在,我們已經(jīng)做好了固定的“倉庫”,來保存我們平時不動的東西。那么,我們要怎么把它拿出來為我所用呢?這時候,readConfig.py文件出世了,它成功的幫我們解決了這個問題,下面就讓我們來一睹它的廬山真面目吧。
import os
import codecs
import configparser
proDir = os.path.split(os.path.realpath(__file__))[0]
configPath = os.path.join(proDir, "config.ini")
class ReadConfig:
def __init__(self):
fd = open(configPath)
data = fd.read()
# remove BOM
if data[:3] == codecs.BOM_UTF8:
data = data[3:]
file = codecs.open(configPath, "w")
file.write(data)
file.close()
fd.close()
self.cf = configparser.ConfigParser()
self.cf.read(configPath)
def get_email(self, name):
value = self.cf.get("EMAIL", name)
return value
def get_http(self, name):
value = self.cf.get("HTTP", name)
return value
def get_db(self, name):
value = self.cf.get("DATABASE", name)
return value
怎么樣,是不是看著很簡單啊,我們定義的方法:根據(jù)名稱取對應(yīng)的值。是不是so easy?!當(dāng)然了,這里我們只用到了get方法,還有其他的例如:set方法,有興趣的同學(xué)可以自己去探索下。
話不多說,我們先來看下common到底有哪些東西。

既然配置文件和讀取配置文件我們都已經(jīng)完成了,也看到了common里的內(nèi)容,接下來就可以寫common里的共通方法了,從哪個下手呢?今天,我們就來翻“Log.py”的牌吧,因為它是比較獨立的,我們單獨跟他打交道,也為了以后它能為我們服務(wù)打下良好基礎(chǔ)。
5、common文件的共通文件
1)log文件
這里呢,我想跟大家多說兩句,對于這個log文件呢,我給它單獨啟用了一個線程,這樣在整個運行過程中,我們在寫log的時候也會比較方便,看名字大家也知道了,這里就是我們對輸出的日志的所有操作了,主要是對輸出格式的規(guī)定,輸出等級的定義以及其他一些輸出的定義等等。
總之,你想對log做的任何事情,都可以放到這里來。我們來看下代碼,沒有比這個更直接有效的了。
import logging
from datetime import datetime
import threading
首先,我們要像上面那樣,引入需要的模塊,才能進行接下來的操作。
class Log:
def __init__(self):
global logPath, resultPath, proDir
proDir = readConfig.proDir
resultPath = os.path.join(proDir, "result")
# create result file if it doesn't exist
if not os.path.exists(resultPath):
os.mkdir(resultPath)
# defined test result file name by localtime
logPath = os.path.join(resultPath, str(datetime.now().strftime("%Y%m%d%H%M%S")))
# create test result file if it doesn't exist
if not os.path.exists(logPath):
os.mkdir(logPath)
# defined logger
self.logger = logging.getLogger()
# defined log level
self.logger.setLevel(logging.INFO)
# defined handler
handler = logging.FileHandler(os.path.join(logPath, "output.log"))
# defined formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# defined formatter
handler.setFormatter(formatter)
# add handler
self.logger.addHandler(handler)
現(xiàn)在,我們創(chuàng)建了上面的Log類,在init初始化方法中,我們進行了log的相關(guān)初始化操作。這樣,log的基本格式已經(jīng)定義完成了,至于其他的方法,就靠大家自己發(fā)揮了,畢竟每個人的需求也不同,我們就只寫普遍的共用方法啦。
接下來,就是把它放進一個線程內(nèi)了,請看下面的代碼:
class MyLog:
log = None
mutex = threading.Lock()
def __init__(self):
pass
@staticmethod
def get_log():
if MyLog.log is None:
MyLog.mutex.acquire()
MyLog.log = Log()
MyLog.mutex.release()
return MyLog.log
看起來是不是沒有想象中的那樣復(fù)雜?。抗?,就是這樣簡單。用python做測試比java要簡單許多。這也是我為什么選擇它的原因。
2)配置接口文件
下面,我們繼續(xù)搭建,這次要做的,是configHttp.py的內(nèi)容。沒錯,我們開始配置接口文件啦?。ńK于寫到接口了,是不是很開心啊~)
下面是接口文件中主要部分的內(nèi)容,讓我們一起來看看吧。
import requests
import readConfig as readConfig
from common.Log import MyLog as Log
localReadConfig = readConfig.ReadConfig()
class ConfigHttp:
def __init__(self):
global host, port, timeout
host = localReadConfig.get_http("baseurl")
port = localReadConfig.get_http("port")
timeout = localReadConfig.get_http("timeout")
self.log = Log.get_log()
self.logger = self.log.get_logger()
self.headers = {}
self.params = {}
self.data = {}
self.url = None
self.files = {}
def set_url(self, url):
self.url = host + url
def set_headers(self, header):
self.headers = header
def set_params(self, param):
self.params = param
def set_data(self, data):
self.data = data
def set_files(self, file):
self.files = file
# defined http get method
def get(self):
try:
response = requests.get(self.url, params=self.params, headers=self.headers, timeout=float(timeout))
# response.raise_for_status()
return response
except TimeoutError:
self.logger.error("Time out!")
return None
# defined http post method
def post(self):
try:
response = requests.post(self.url, headers=self.headers, data=self.data, files=self.files, timeout=float(timeout))
# response.raise_for_status()
return response
except TimeoutError:
self.logger.error("Time out!")
return None
這里我們就挑重點來說吧。首先,可以看到,小編這次是用Python自帶的requests來進行接口測試的,相信有心的朋友已經(jīng)看出來了,Python+requests這個模式是很好用的,它已經(jīng)幫我們封裝好了測試接口的方法,用起來很方便。這里呢,我就拿get和post兩個方法來說吧。(平時用的最多的就是這兩個方法了,其他方法,大家可以仿照著自行擴展)
get方法
接口測試中見到最多的就是get方法和post方法,其中,get方法用于獲取接口的測試,說白了,就是使用get的接口,不會對后臺數(shù)據(jù)進行更改。
對于requests提供的get方法,有幾個常用的參數(shù):
url:顯而易見,就是接口的地址url啦
headers:定制請求頭(headers),例如:content-type = application/x-www-form-urlencoded
params:用于傳遞測試接口所要用的參數(shù),這里我們用Python中的字典形式(key:value)進行參數(shù)的傳遞
timeout:設(shè)置接口連接的最大時間(超過該時間會拋出超時錯誤)
現(xiàn)在,各個參數(shù)我們已經(jīng)知道是什么意思了,剩下的就是往里面填值啦,是不是機械式的應(yīng)用啊,哈哈,小編我就是這樣機械般的學(xué)習(xí)的啦~
舉個栗子:
url=‘http://api.shein.com/v2/member/logout’
header={‘content-type’: application/x-www-form-urlencoded}
param={‘user_id’: 123456,‘email’: 123456@163.com}
timeout=0.5
requests.get(url, headers=header, params=param, timeout=timeout)
post方法
與get方法類似,只要設(shè)置好對應(yīng)的參數(shù),就可以了。下面就直接舉個栗子,直接上代碼吧:
url=‘http://api.shein.com/v2/member/login’
header={‘content-type’: application/x-www-form-urlencoded}
data={‘email’: 123456@163.com,‘password’: 123456}
timeout=0.5
requests.post(url, headers=header, data=data, timeout=timeout)
怎么樣,是不是也很簡單啊。這里我們需要說明一下,post方法中的參數(shù),我們不在使用params進行傳遞,而是改用data進行傳遞了。
6、接口返回值
1)常用返回值
哈哈哈,終于說完啦,下面我們來探討下接口的返回值。依然只說常用的返回值的操作。
text:獲取接口返回值的文本格式
json():獲取接口返回值的json()格式
status_code:返回狀態(tài)碼(成功為:200)
headers:返回完整的請求頭信息(headers['name']:返回指定的headers內(nèi)容)
encoding:返回字符編碼格式url:返回接口的完整url地址
以上這些,就是常用的方法啦,大家可自行取之。
2)拋出異常的解決辦法
關(guān)于失敗請求拋出異常,我們可以使用“raise_for_status()”來完成,那么,當(dāng)我們的請求發(fā)生錯誤時,就會拋出異常。在這里提醒下各位朋友,如果你的接口,在地址不正確的時候,會有相應(yīng)的錯誤提示(有時也需要進行測試),這時,千萬不能使用這個方法來拋出錯誤,因為Python自己在鏈接接口時就已經(jīng)把錯誤拋出,那么,后面你將無法測試期望的內(nèi)容。而且程序會直接在這里當(dāng)?shù)?,以錯誤來計。(別問我怎么知道的,因為我就是測試的時候發(fā)現(xiàn)的)
7、common.py的內(nèi)容
好了。接口文件也講完了,是不是感覺離成功不遠(yuǎn)了呢?嗯,如果各位已經(jīng)看到了這里,那么恭喜大家,下面還有很長的路要走~
下面,我們一起來學(xué)習(xí)common.py里的內(nèi)容。
import os
from xlrd import open_workbook
from xml.etree import ElementTree as ElementTree
from common.Log import MyLog as Log
localConfigHttp = configHttp.ConfigHttp()
log = Log.get_log()
logger = log.get_logger()
# 從excel文件中讀取測試用例
def get_xls(xls_name, sheet_name):
cls = []
# get xls file's path
xlsPath = os.path.join(proDir, "testFile", xls_name)
# open xls file
file = open_workbook(xlsPath)
# get sheet by name
sheet = file.sheet_by_name(sheet_name)
# get one sheet's rows
nrows = sheet.nrows
for i in range(nrows):
if sheet.row_values(i)[0] != u'case_name':
cls.append(sheet.row_values(i))
return cls
# 從xml文件中讀取sql語句
database = {}
def set_xml():
if len(database) == 0:
sql_path = os.path.join(proDir, "testFile", "SQL.xml")
tree = ElementTree.parse(sql_path)
for db in tree.findall("database"):
db_name = db.get("name")
# print(db_name)
table = {}
for tb in db.getchildren():
table_name = tb.get("name")
# print(table_name)
sql = {}
for data in tb.getchildren():
sql_id = data.get("id")
# print(sql_id)
sql[sql_id] = data.text
table[table_name] = sql
database[db_name] = table
def get_xml_dict(database_name, table_name):
set_xml()
database_dict = database.get(database_name).get(table_name)
return database_dict
def get_sql(database_name, table_name, sql_id):
db = get_xml_dict(database_name, table_name)
sql = db.get(sql_id)
return sql
上面就是我們common的兩大主要內(nèi)容了,什么?還不知道是什么嗎?讓我告訴你吧。
1.我們利用xml.etree.Element來對xml文件進行操作,然后通過我們自定義的方法,根據(jù)傳遞不同的參數(shù)取得不(想)同(要)的值。
2.利用xlrd來操作excel文件,注意啦,我們是用excel文件來管理測試用例的。
聽起來會不會有點兒懵,小編剛學(xué)時也很懵,看文件就好理解了。
excel文件:

xml文件:

至于具體的方法,我就不再贅述了。
8、數(shù)據(jù)庫
接下來,我們看看數(shù)據(jù)庫和發(fā)送郵件吧(也可根據(jù)需要,不寫該部分內(nèi)容)。先看老朋友“數(shù)據(jù)庫”吧,小編這次使用的是MySQL數(shù)據(jù)庫,所以我們就以它為例吧。
import pymysql
import readConfig as readConfig
from common.Log import MyLog as Log
localReadConfig = readConfig.ReadConfig()
class MyDB:
global host, username, password, port, database, config
host = localReadConfig.get_db("host")
username = localReadConfig.get_db("username")
password = localReadConfig.get_db("password")
port = localReadConfig.get_db("port")
database = localReadConfig.get_db("database")
config = {
'host': str(host),
'user': username,
'passwd': password,
'port': int(port),
'db': database
}
def __init__(self):
self.log = Log.get_log()
self.logger = self.log.get_logger()
self.db = None
self.cursor = None
def connectDB(self):
try:
# connect to DB
self.db = pymysql.connect(**config)
# create cursor
self.cursor = self.db.cursor()
print("Connect DB successfully!")
except ConnectionError as ex:
self.logger.error(str(ex))
def executeSQL(self, sql, params):
self.connectDB()
# executing sql
self.cursor.execute(sql, params)
# executing by committing to DB
self.db.commit()
return self.cursor
def get_all(self, cursor):
value = cursor.fetchall()
return value
def get_one(self, cursor):
value = cursor.fetchone()
return value
def closeDB(self):
self.db.close()
print("Database closed!")
這就是完整的數(shù)據(jù)庫的文件啦。因為小編的需求對數(shù)據(jù)庫的操作不是很復(fù)雜,所以這些已基本滿足要求啦。注意下啦,在此之前,請朋友們先把pymysql裝起來!
安裝的方法很簡單,由于小編是使用pip來管理Python包安裝的,所以只要進入Python安裝路徑下的pip文件夾下,執(zhí)行以下命令即可:
pip install pymysql
這樣,我們就可以利用Python鏈接數(shù)據(jù)庫啦~
小伙伴們發(fā)現(xiàn)沒,在整個文件中,我們并沒有出現(xiàn)具體的變量值哦,為什么呢?沒錯,因為前面我們寫了config.ini文件,所有的數(shù)據(jù)庫配置信息都在這個文件內(nèi)哦,是不是感覺很方便呢,以后就算變更數(shù)據(jù)庫了,也只要修改config.ini文件的內(nèi)容就可以了,結(jié)合前面測試用例的管理(excel文件),sql語句的存放(xml文件),還有接下來我們要說的,businessCommon.py和存放具體case的文件夾,那么我們就已經(jīng)將數(shù)據(jù)和業(yè)務(wù)分開啦,哈哈哈,想想以后修改測試用例內(nèi)容,sql語句神馬的工作,再也不用每個case都修改,只要改幾個固定的文件,是不是頓時開心了呢?
總結(jié):
回歸上面的configDB.py文件,內(nèi)容很簡單,相信大家都能看得懂,就是連接數(shù)據(jù)庫,執(zhí)行sql,獲取結(jié)果,最后關(guān)閉數(shù)據(jù)庫,沒有什么不一樣的地方。
再來談?wù)勦]件。你是不是也遇到過這樣的問題:每次測試完之后,都需要給開發(fā)一份測試報告。那么,對于我這樣的懶人,是不愿意老是找人家開發(fā)的,所以,我就想,每次測試完,我們可以讓程序自己給開發(fā)人員發(fā)一封e-mail,告訴他們,測試已經(jīng)結(jié)束了,并且把測試報告以附件的形式,通過e-mail發(fā)送給開發(fā)者的郵箱,這樣效率迅速提升。
所以,configEmail.py應(yīng)運而生。請看:
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from datetime import datetime
import threading
import readConfig as readConfig
from common.Log import MyLog
import zipfile
import glob
localReadConfig = readConfig.ReadConfig()
class Email:
def __init__(self):
global host, user, password, port, sender, title, content
host = localReadConfig.get_email("mail_host")
user = localReadConfig.get_email("mail_user")
password = localReadConfig.get_email("mail_pass")
port = localReadConfig.get_email("mail_port")
sender = localReadConfig.get_email("sender")
title = localReadConfig.get_email("subject")
content = localReadConfig.get_email("content")
self.value = localReadConfig.get_email("receiver")
self.receiver = []
# get receiver list
for n in str(self.value).split("/"):
self.receiver.append(n)
# defined email subject
date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.subject = title + " " + date
self.log = MyLog.get_log()
self.logger = self.log.get_logger()
self.msg = MIMEMultipart('mixed')
def config_header(self):
self.msg['subject'] = self.subject
self.msg['from'] = sender
self.msg['to'] = ";".join(self.receiver)
def config_content(self):
content_plain = MIMEText(content, 'plain', 'utf-8')
self.msg.attach(content_plain)
def config_file(self):
# if the file content is not null, then config the email file
if self.check_file():
reportpath = self.log.get_result_path()
zippath = os.path.join(readConfig.proDir, "result", "test.zip")
# zip file
files = glob.glob(reportpath + '\*')
f = zipfile.ZipFile(zippath, 'w', zipfile.ZIP_DEFLATED)
for file in files:
f.write(file)
f.close()
reportfile = open(zippath, 'rb').read()
filehtml = MIMEText(reportfile, 'base64', 'utf-8')
filehtml['Content-Type'] = 'application/octet-stream'
filehtml['Content-Disposition'] = 'attachment; filename="test.zip"'
self.msg.attach(filehtml)
def check_file(self):
reportpath = self.log.get_report_path()
if os.path.isfile(reportpath) and not os.stat(reportpath) == 0:
return True
else:
return False
def send_email(self):
self.config_header()
self.config_content()
self.config_file()
try:
smtp = smtplib.SMTP()
smtp.connect(host)
smtp.login(user, password)
smtp.sendmail(sender, self.receiver, self.msg.as_string())
smtp.quit()
self.logger.info("The test report has send to developer by email.")
except Exception as ex:
self.logger.error(str(ex))
class MyEmail:
email = None
mutex = threading.Lock()
def __init__(self):
pass
@staticmethod
def get_email():
if MyEmail.email is None:
MyEmail.mutex.acquire()
MyEmail.email = Email()
MyEmail.mutex.release()
return MyEmail.email
if __name__ == "__main__":
email = MyEmail.get_email()