「手把手教python3接口自動化」: 搭建接口自動化測試框架

「第二十六章」 搭建接口自動化測試框架

26.1 為什麼要開發Python3 接口測試框架

先看教育局招生管理系統登錄接口用例。

import requests

from bs4 import BeautifulSoup

import xlrd

import json

def readExcel(rowx, filePath='data.xls'):

'''

讀取excel中數據並且返回

:parameter filePath:xlsx文件名稱

:parameter rowx:在excel中的行數

'''

book = xlrd.open_workbook(filePath)

sheet = book.sheet_by_index(0)

return sheet.row_values(rowx)

#提取第一個測試用例數據

print("第一行數據內容:",readExcel(2))

#查看數據的類型

print("數據類型:{0}:".format(type(readExcel(2))))

# 從列表中提取url

url = readExcel(2)[3]

print(url)

# 從列表中提取data參數

# 由於JSON中,標準語法中,不支持單引號,屬性或者屬性值,都必須是雙引號括起來,用字符串方法replace("'",'\"')進行處理

data1 =readExcel(2)[4].replace("'",'\"')

#因為請求參數數據類型是字典,所以進行了反序列化的處理。

data = json.loads(data1)

print(data)

print(type(data))

print("---------------------------------------------")

#招生系統接口例子

headers = {

"Connection": "keep-alive",

"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",

"Referer": "http://127.0.0.1:8090/recruit.students/login/view",

}

# URL參數

payload = data

# 發送get請求

response = requests.get(url,headers = headers,params=payload)

# 打印請求response後的URL

print(response.url)

# 查看響應內容,response.text 返回的是Unicode格式的數據

print(response.text)

# 查看響應碼

print(response.status_code)

從教育局招生管理系統登錄接口用例中,我們看到,要完成一個接口測試,需要寫很多行的代碼,一個完整的系統測試,測試用例可能有100~1000個,那麼如果每個用例都一個一個這樣寫,那麼編寫用例和維護用例的成本就很高,而且代碼很多,不直觀。

隨著自動化測試的不斷推廣,各家軟件科技公司也不斷的引進了專門的自動化測試人員,一個完整的系統測試,測試用例中有許多共用的模塊和方法,不需要每個人都重複去寫,這就需要有資深的測試專家去規劃一個項目的自動化測試框架。

同時,接口測試生成測試報告,發送給相關的領導,也是需要一個完整的接口自動化測試框架。

26.2 Python3 接口自動化測試框架介紹

a) 基於 python 腳本編寫,不需要藉助其他工具,環境配置簡單。

b) 採用數據驅動測試的方式,後期僅僅只需要維護一個測試數據。

c) 測試用例可以分模塊編寫,方便對用例的管理維護。

d) 測試用例和測試執行分離,實現執行測試時用例可以任意配置。

e) 實現多線程執行,可以同時運行多個測試集。

f) 測試結束後直接生成測試報告,統計測試執行情況,執行情況詳細,能夠快速定位問題,並且容易擴展優化。

26.3 框架流程說明

「手把手教python3接口自動化」: 搭建接口自動化測試框架

26.4 框架的目錄結構

「手把手教python3接口自動化」: 搭建接口自動化測試框架

【common】:存放公共的方法。

【result】:執行過程中生成的文件夾,裡面存放每次測試的結果。

【testCase】:用於存放具體的測試case。

【testFile】:存放測試過程中用到的文件,包括上傳的文件,測試用例數據驅動以及數據庫的sql語句。

【caselist】:caselist.txt文件,配置每次執行的case名稱。

【config】:配置一些常量,例如數據庫的相關信息,接口的相關信息等。

【readConfig】:用於讀取config配置文件中的內容

【runAll】:用於執行case,生成測試報告。

上面是整個Python3接口測試框架的目錄結構,下面對每一塊進行介紹。

26.5 config.ini 和 readConfig.py

ini 文件其實就是所謂的初始化配置文件,一般的格式為:

[SECTION0]

key0 = value0

key1 = value1

config.ini 文件的內容:

[EMAIL]

mail_host = smtp.163.com

mail_user = [email protected]

mail_pass = 123456

mail_port = 25

sender = [email protected]

receiver = [email protected]/[email protected]

subject = Interface Test Report

content = "All interface test has been complited\\nplease read the report file about the detile of result in the attachment."

testuser = Someone

on_off = off

[HTTP]

scheme = http

baseurl = www.baidu.com

port = 8080

timeout = 10.0

[HEADERS]

siteuid = all

clientid = 100

token_v = 213612368807_5811a363c85b19.22399915_75e4ee2761d60f2d7597eaec2579297f1cd7f6e3

token_u = 320012369021_586b1c65c3b3c1.51719831_e76832dc51f7ec8de0ba6ecdd69c8b7658dee93c

[DATABASE]

host = localhost

username = root

password = root

port = 3306

database = test

這個文件是整個Python3接口測試框架的配置文件,主要用來配置一些固定不變的參數。我們在測試用例中使用這些配置文件的內容,就需要使用readConfig.py去讀取。

readConfig.py文件的代碼。

import os

#導入codecs模塊,使用codecs模塊進行文件操作

import codecs

#導入configparser模塊,python3 用ConfigParser包處理 ini文件

import configparser

#獲取config.ini 文件路徑

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_headers(self, name):

value = self.cf.get("HEADERS", name)

return value

def set_headers(self, name, value):

self.cf.set("HEADERS", name, value)

with open(configPath, 'w+') as f:

self.cf.write(f)

def get_url(self, name):

value = self.cf.get("URL", name)

return value

def get_db(self, name):

value = self.cf.get("DATABASE", name)

return value

readConfig.py文件定義了相應的方法,根據名稱取對應的值。

26.6 common 文件夾

配置文件和讀取配置文件已經介紹,接下來就可以寫common裡的共通方法。

【Log.py】:Log 模塊是一個獨立的模塊,對輸出的日誌的所有操作。

【configHttp.py】:configHttp 模塊是接口配置文件。

【common】:common 模塊 是數據驅動的配置文件。

【configDB 模塊】:configDB 模塊主要是封裝了操作mysql數據的相關操作方法。

【configEmail 模塊】:configEmail 模塊主要是封裝了發送郵件的相關方法。

【businessCommon 模塊】:businessCommon 模塊封裝了登錄和登錄退出的方法。

26.6.1 Log.py 模塊

Log.py 模塊主要是對輸出格式的規定,輸出等級的定義以及其他一些輸出的定義等。

import os,sys

import readConfig as readConfig

import logging

from datetime import datetime

import threading

localReadConfig = readConfig.ReadConfig()

class Log:

def __init__(self):

global logPath, resultPath, proDir

proDir = readConfig.proDir

resultPath = os.path.join(proDir, "result")

if not os.path.exists(resultPath):

os.mkdir(resultPath)

logPath = os.path.join(resultPath, str(datetime.now().strftime("%Y%m%d%H%M%S")))

if not os.path.exists(logPath):

os.mkdir(logPath)

self.logger = logging.getLogger()

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')

handler.setFormatter(formatter)

self.logger.addHandler(handler)

def get_logger(self):

"""

get logger

:return:

"""

return self.logger

def build_start_line(self, case_no):

"""

write start line

:return:

"""

self.logger.info("--------" + case_no + " START--------")

def build_end_line(self, case_no):

"""

write end line

:return:

"""

self.logger.info("--------" + case_no + " END--------")

def build_case_line(self, case_name, code, msg):

"""

write test case line

:param case_name:

:param code:

:param msg:

:return:

"""

self.logger.info(case_name+" - Code:"+code+" - msg:"+msg)

def get_report_path(self):

"""

get report file path

:return:

"""

report_path = os.path.join(logPath, "report.html")

return report_path

def get_result_path(self):

"""

get test result path

:return:

"""

return logPath

def write_result(self, result):

"""

:param result:

:return:

"""

result_path = os.path.join(logPath, "report.txt")

fb = open(result_path, "wb")

try:

fb.write(result)

except FileNotFoundError as ex:

logger.error(str(ex))

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

if __name__ == "__main__":

log = MyLog.get_log()

logger = log.get_logger()

logger.debug("test debug")

logger.info("test info")

26.6.2 configHttp.py 模塊

configHttp.py 模塊代碼:

import requests

import readConfig as readConfig

from common.Log import MyLog as Log

import json

localReadConfig = readConfig.ReadConfig()

class ConfigHttp:

def __init__(self):

global scheme, host, port, timeout

scheme = localReadConfig.get_http("scheme")

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 = {}

self.state = 0

def set_url(self, url):

"""

set url

:param: interface url

:return:

"""

self.url = scheme+'://'+host+url

def set_headers(self, header):

"""

set headers

:param header:

:return:

"""

self.headers = header

def set_params(self, param):

"""

set params

:param param:

:return:

"""

self.params = param

def set_data(self, data):

"""

set data

:param data:

:return:

"""

self.data = data

def set_files(self, filename):

"""

set upload files

:param filename:

:return:

"""

if filename != '':

file_path = 'F:/AppTest/Test/interfaceTest/testFile/img/' + filename

self.files = {'file': open(file_path, 'rb')}

if filename == '' or filename is None:

self.state = 1

# defined http get method

def get(self):

"""

defined get method

:return:

"""

try:

response = requests.get(self.url, headers=self.headers, params=self.params, timeout=float(timeout))

# response.raise_for_status()

return response

except TimeoutError:

self.logger.error("Time out!")

return None

# defined http post method

# include get params and post data

# uninclude upload file

def post(self):

"""

defined post method

:return:

"""

try:

response = requests.post(self.url, headers=self.headers, params=self.params, data=self.data, timeout=float(timeout))

# response.raise_for_status()

return response

except TimeoutError:

self.logger.error("Time out!")

return None

# defined http post method

# include upload file

def postWithFile(self):

"""

defined post method

:return:

"""

try:

response = requests.post(self.url, headers=self.headers, data=self.data, files=self.files, timeout=float(timeout))

return response

except TimeoutError:

self.logger.error("Time out!")

return None

# defined http post method

# for json

def postWithJson(self):

"""

defined post method

:return:

"""

try:

response = requests.post(self.url, headers=self.headers, json=self.data, timeout=float(timeout))

return response

except TimeoutError:

self.logger.error("Time out!")

return None

if __name__ == "__main__":

print("ConfigHTTP")

configHttp.py 模塊說明:

接口測試框架用python3自帶的requests來進行接口請求測試。裡面封裝了get和post兩個方法。

【get方法】

對於requests提供的get方法,有幾個常用的參數:

url:接口的url地址。

headers:定製請求頭(headers),例如:content-type = application/x-www-form-urlencoded。

params:用於傳遞測試接口所要用的參數,這裡我們用python中的字典形式(key:value)進行參數的傳遞。

timeout:設置接口連接的最大時間(超過該時間會拋出超時錯誤)

例子:

url=‘http://api.shein.com/v2/member/logout’

header={‘content-type’: application/x-www-form-urlencoded}

param={‘user_id’: 123456,‘email’: [email protected]}

timeout=0.5

requests.get(url, headers=header, params=param, timeout=timeout)

【post方法】

與get方法類似,只要設置好對應的參數,就可以了

例子:

url=‘http://api.shein.com/v2/member/login’

header={‘content-type’: application/x-www-form-urlencoded}

data={‘email’: [email protected],‘password’: 123456}

timeout=0.5

requests.post(url, headers=header, data=data, timeout=timeout)

備註:post 方法中的參數,我們不是使用 params 進行傳遞,而是改用 data 進行傳遞。

26.6.3 common.py 模塊

common.py 模塊代碼:

import requests

import readConfig as readConfig

import os

from xlrd import open_workbook

from xml.etree import ElementTree as ElementTree

from common import configHttp as configHttp

from common.Log import MyLog as Log

import json

localReadConfig = readConfig.ReadConfig()

proDir = readConfig.proDir

localConfigHttp = configHttp.ConfigHttp()

log = Log.get_log()

logger = log.get_logger()

caseNo = 0

def get_visitor_token():

"""

create a token for visitor

:return:

"""

host = localReadConfig.get_http("BASEURL")

response = requests.get(host+"/v2/User/Token/generate")

info = response.json()

token = info.get("info")

logger.debug("Create token:%s" % (token))

return token

def set_visitor_token_to_config():

"""

set token that created for visitor to config

:return:

"""

token_v = get_visitor_token()

localReadConfig.set_headers("TOKEN_V", token_v)

def get_value_from_return_json(json, name1, name2):

"""

get value by key

:param json:

:param name1:

:param name2:

:return:

"""

info = json['info']

group = info[name1]

value = group[name2]

return value

def show_return_msg(response):

"""

show msg detail

:param response:

:return:

"""

url = response.url

msg = response.text

print("\\n請求地址:"+url)

# 可以顯示中文

print("\\n請求返回值:"+'\\n'+json.dumps(json.loads(msg), ensure_ascii=False, sort_keys=True, indent=4))

# ****************************** read testCase excel ********************************

def get_xls(xls_name, sheet_name):

"""

get interface data from xls file

:return:

"""

cls = []

# get xls file's path

xlsPath = os.path.join(proDir, "testFile", 'case', 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

# ****************************** read SQL xml ********************************

database = {}

def set_xml():

"""

set sql xml

:return:

"""

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):

"""

get db dict by given name

:param database_name:

:param table_name:

:return:

"""

set_xml()

database_dict = database.get(database_name).get(table_name)

return database_dict

def get_sql(database_name, table_name, sql_id):

"""

get sql by given name and sql_id

:param database_name:

:param table_name:

:param sql_id:

:return:

"""

db = get_xml_dict(database_name, table_name)

sql = db.get(sql_id)

return sql

# ****************************** read interfaceURL xml ********************************

def get_url_from_xml(name):

"""

By name get url from interfaceURL.xml

:param name: interface's url name

:return: url

"""

url_list = []

url_path = os.path.join(proDir, 'testFile', 'interfaceURL.xml')

tree = ElementTree.parse(url_path)

for u in tree.findall('url'):

url_name = u.get('name')

if url_name == name:

for c in u.getchildren():

url_list.append(c.text)

url = '/v2/' + '/'.join(url_list)

return url

if __name__ == "__main__":

print(get_xls("login"))

set_visitor_token_to_config()

common.py 模塊主要是封裝了數據驅動的各種方法(讀取xml文件、讀取excel文件,讀取mysql數據庫數據),測試用例就是通過 excel 文件來管理測試用例的。

「手把手教python3接口自動化」: 搭建接口自動化測試框架

26.6.4 configDB 模塊

configDB 模塊代碼:

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):

"""

connect to database

:return:

"""

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):

"""

execute sql

:param sql:

:return:

"""

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):

"""

get all result after execute sql

:param cursor:

:return:

"""

value = cursor.fetchall()

return value

def get_one(self, cursor):

"""

get one result after execute sql

:param cursor:

:return:

"""

value = cursor.fetchone()

return value

def closeDB(self):

"""

close database

:return:

"""

self.db.close()

print("Database closed!")

configDB 模塊主要是封裝了操作mysql數據的相關操作方法(連接數據庫,執行sql,獲取結果,最後關閉數據庫)。

26.6.5 configEmail 模塊

configEmail 模塊代碼:

import os

import smtplib

from email.mime.multipart import MIMEMultipart

from email.mime.text import MIMEText

from email.mime.image import MIMEImage

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

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")

# get receiver list

self.value = localReadConfig.get_email("receiver")

self.receiver = []

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 = "接口測試報告" + " " + date

self.log = MyLog.get_log()

self.logger = self.log.get_logger()

self.msg = MIMEMultipart('related')

def config_header(self):

"""

defined email header include subject, sender and receiver

:return:

"""

self.msg['subject'] = self.subject

self.msg['from'] = sender

self.msg['to'] = ";".join(self.receiver)

def config_content(self):

"""

write the content of email

:return:

"""

f = open(os.path.join(readConfig.proDir, 'testFile', 'emailStyle.txt'))

content = f.read()

f.close()

content_plain = MIMEText(content, 'html', 'UTF-8')

self.msg.attach(content_plain)

self.config_image()

def config_image(self):

"""

config image that be used by content

:return:

"""

# defined image path

image1_path = os.path.join(readConfig.proDir, 'testFile', 'img', '1.png')

fp1 = open(image1_path, 'rb')

msgImage1 = MIMEImage(fp1.read())

# self.msg.attach(msgImage1)

fp1.close()

# defined image id

msgImage1.add_header('Content-ID', '<image1>')/<image1>

self.msg.attach(msgImage1)

image2_path = os.path.join(readConfig.proDir, 'testFile', 'img', 'logo.jpg')

fp2 = open(image2_path, 'rb')

msgImage2 = MIMEImage(fp2.read())

# self.msg.attach(msgImage2)

fp2.close()

# defined image id

msgImage2.add_header('Content-ID', '<image2>')/<image2>

self.msg.attach(msgImage2)

def config_file(self):

"""

config email file

:return:

"""

# 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, '/report/'+os.path.basename(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):

"""

check test report

:return:

"""

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):

"""

send email

:return:

"""

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()

configEmail 模塊主要是封裝了發送郵件的相關方法(每次測試完之後,都會生成一份測試報告,並且把測試報告以附件的形式,通過email發送給相關的郵箱)。

26.6.6 businessCommon 模塊

businessCommon 模塊代碼:

from common import common

from common import configHttp

import readConfig as readConfig

localReadConfig = readConfig.ReadConfig()

localConfigHttp = configHttp.ConfigHttp()

localLogin_xls = common.get_xls("userCase.xlsx", "login")

localAddAddress_xls = common.get_xls("userCase.xlsx", "addAddress")

# login

def login():

"""

login

:return: token

"""

# set url

url = common.get_url_from_xml('login')

localConfigHttp.set_url(url)

# set header

token = localReadConfig.get_headers("token_v")

header = {"token": token}

localConfigHttp.set_headers(header)

# set param

data = {"email": localLogin_xls[0][3],

"password": localLogin_xls[0][4]}

localConfigHttp.set_data(data)

# login

response = localConfigHttp.post().json()

token = common.get_value_from_return_json(response, "member", "token")

return token

# logout

def logout(token):

"""

logout

:param token: login token

:return:

"""

# set url

url = common.get_url_from_xml('logout')

localConfigHttp.set_url(url)

# set header

header = {'token': token}

localConfigHttp.set_headers(header)

# logout

localConfigHttp.get()

businessCommon 模塊封裝了登錄和登錄退出的方法,因為每個測試用例都用到。

26.7 runAll 文件

runAll 文件代碼:

import os

import unittest

import time as t

from common.Log import MyLog as Log

import readConfig as readConfig

import HTMLTestRunner

from common.configEmail import MyEmail

localReadConfig = readConfig.ReadConfig()

class AllTest:

def __init__(self):

global log, logger, resultPath, on_off

log = Log.get_log()

logger = log.get_logger()

resultPath = log.get_report_path()

on_off = localReadConfig.get_email("on_off")

self.caseListFile = os.path.join(readConfig.proDir, "caselist.txt")

self.caseFile = os.path.join(readConfig.proDir, "testCase")

# self.caseFile = None

self.caseList = []

self.email = MyEmail.get_email()

def set_case_list(self):

"""

set case list

:return:

"""

fb = open(self.caseListFile)

for value in fb.readlines():

data = str(value)

if data != '' and not data.startswith("#"):

self.caseList.append(data.replace("\\n", ""))

fb.close()

def set_case_suite(self):

"""

set case suite

:return:

"""

self.set_case_list()

test_suite = unittest.TestSuite()

suite_module = []

for case in self.caseList:

case_name = case.split("/")[-1]

print(case_name+".py")

discover = unittest.defaultTestLoader.discover(self.caseFile, pattern=case_name + '.py', top_level_dir=None)

suite_module.append(discover)

if len(suite_module) > 0:

for suite in suite_module:

for test_name in suite:

test_suite.addTest(test_name)

else:

return None

return test_suite

def run(self):

"""

run test

:return:

"""

try:

suit = self.set_case_suite()

if suit is not None:

logger.info("********TEST START********")

fp = open(resultPath, 'wb')

runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report', description='Test Description')

runner.run(suit)

else:

logger.info("Have no case to test.")

except Exception as ex:

logger.error(str(ex))

finally:

logger.info("*********TEST END*********")

fp.close()

# send test report by email

if on_off == 'on':

self.email.send_email()

elif on_off == 'off':

logger.info("Doesn't send report email to developer.")

else:

logger.info("Unknow state.")

if __name__ == '__main__':

obj = AllTest()

obj.run()

runAll的執行原理,首先我們要從 caselist.txt 文件中讀取需要執行的 case 名稱,然後將他們添加到 python3 自帶的 unittest 測試集的測試套件中,最後執行run()函數,執行測試集。

26.8 result 文件

「手把手教python3接口自動化」: 搭建接口自動化測試框架

result文件夾會在首次執行case時生成,並且以後的測試結果都會被保存在該文件夾下,同時每次測試的文件夾都是用系統時間命名,裡面包含了兩個文件,log文件和測試報告。

26.9 caselist.txt文件

「手把手教python3接口自動化」: 搭建接口自動化測試框架

caselist.txt文件存放了要執行的測試用例名稱,凡是沒有被註釋掉的,都是要被執行的case名稱。

26.10 testFile文件夾

「手把手教python3接口自動化」: 搭建接口自動化測試框架

testFile文件夾下,case文件夾下放置我們測試時用來管理測試用例, img下放用於數據庫查詢的sql語句的xml文件、郵件的樣式等。

26.11 testCase文件夾

「手把手教python3接口自動化」: 搭建接口自動化測試框架

testCase文件夾下,存放我們寫的具體的測試用例,測試用例目錄可以根據模塊來劃分,也可以根據其他特定需求來劃分。

注意:所有的 case 名稱都要以 test 開頭來命名,這是因為 Python3 unittest 在進行測試時會自動匹配 testCase 文件夾下面所有 test 開頭的 .py 文件。

26.12 彙總

對 Python3 的接口測試框架簡單介紹了一下,其實大家要學習的是封裝的思想,如何把一些公共的模塊抽象出來,達到數據與業務流程的分離,在上面的框架中,我把常用固定的參數放到 config.ini 文件中,測試用例的管理(excel文件),sql 語句的存放(xml文件),每個公司的項目不同,要求的不一樣,自己可以根據自己項目的需求去封裝自己的接口測試框架。


分享到:


相關文章: