文章目錄
- 自動化接口測試
- 第一章
- 第二章 接口自動化框架編寫
- 一、項目及框架的搭建:
- 二、接口用例編寫:
- 三、Requests使用:
- 四、配置文件:
- 五、日誌文件:
- 六、pytesy框架:
- 七、結果斷言:
- 八、數據驅動:
- 九、Allure報告:
- 十、郵件配置:
- 三、持續集成與docker介紹及配置:
自動化接口測試
第一章
一、項目相關:
1.項目信息:
1.1 項目介紹:
- 項目名稱: 美多商城
- 項目訪問訪問網站: http://211.103.136.242:8062/
2.接口信息:
2.1 接口介紹:
- 登錄;
- 個人信息;
- 獲取商品信息;
- 購物車;
- 訂單;
2.2 登錄接口文檔:
- 後端接口設計:
- 請求方式: POST /authorizations/
- 請求參數:
參數名 | 類型 | 說明 |
---|---|---|
username | str | 用戶名 |
password | str | 密碼 |
返回數據
返回值 | 類型 | 說明 |
---|---|---|
username | str | 用戶名 |
user_id | int | 用戶id |
token | str | 身份認證 |
2.3 用戶中心個人信息:
訪問必須要求用戶已通過認證(即登錄之後)
認證:
headers:{
'Authorization': 'JWT '+ this.token
}
- 後端接口設計:
- 請求方法: GET /user/
- 返回數據:
返回值 | 類型 | 是否必須 | 說明 |
---|---|---|---|
id | int | 是 | 用戶id |
username | str | 是 | 用戶名 |
mobile | str | 是 | 手機號 |
str | 是 | email郵箱 | |
email_active | bool | 是 | 郵箱是否通過驗證 |
2.4 獲取商品列表數據:
- 業務需求:
需要對商品數量進行分頁支持,並且可以按照時間(默認)、價格、銷量(人氣)進行排序 - 後端接口設計:
- 請求方法: GET /categories/(?P<category_id>\d+)/skus>page=xxx&page_size=xxx&ordering=xxx
- 請求參數:
參數 | 類型 | 是否必須 | 說明 |
---|---|---|---|
category_id | int | 是 | 類別id(第三級類別) |
page | int | 是 | 頁數 |
page_size | int | 是 | 每頁數量 |
ordering | str | 是 | 排序關鍵字(create_time, price, sales) |
返回數據:
返回值 | 類型 | 是否必須 | 說明 |
---|---|---|---|
id | int | 是 | 商品sku編號 |
name | str | 是 | 商品名稱 |
price | decimal | 是 | 單價 |
default_image_url | str | 是 | 默認圖片 |
comments | int | 是 | 評論數量 |
2.5 添加到購物車:
- 後端接口:
- 請求方法: POST /cart/
- 請求參數:
參數 | 類型 | 是否必須 | 說明 |
---|---|---|---|
sku_id | int | 是 | 商品sku_id |
count | int | 是 | 數量 |
selected | bool | 否 | 是否勾選,默認勾選 |
返回數據:
參數 | 類型 | 是否必須 | 說明 |
---|---|---|---|
sku_id | int | 是 | 商品sku_id |
count | int | 是 | 數量 |
selected | bool | 否 | 是否勾選,默認勾選 |
訪問此接口, 無論用戶是否登錄,前端請求都需要帶請求頭Authorization, 由後端判斷是否登錄.
2.6 保存訂單:
- 後端接口設計:
- 請求方式: POST /orders/
- 請求參數:
參數 | 類型 | 是否必須 | 說明 |
---|---|---|---|
address | int | 是 | 收貨地址id |
pat_method | int | 是 | 支付方式 |
返回數據:
參數 | 類型 | 是否必須 | 說明 |
---|---|---|---|
order_id | char | 是 | 訂單編號 |
二、接口測試框架:
1.介紹:
1.1 框架對比:
-
Unittest:
- 不支持失敗自動重新執行;
- 參數化需依賴三方庫;
- HTMLTestRunner三方報告不夠美觀
-
pytest:
- 兼容unittest
- 支持失敗自動重新執行;
- 參數化使用自帶裝飾器;
- 兼容主流allure框架,報告美觀功能強大;
2.流程:
2.1 代碼運行:
2.2 jenkins運行:
)]
第二章 接口自動化框架編寫
一、項目及框架的搭建:
1.工具:
1.1 python:
- 下載地址: https://www.python.org/download
1.2 pycharm:
- 下載地址: https://www.jetbrains.com/pycharm
1.3 git:
- 下載地址: https://git-scm.com/download
2.框架目錄:
2.1 創建目錄:
InterAutoTest_W
### 3.配置(pycharm):
- 配置python環境:
Setting ->Project ->Project Interpreter
- 配置git:
Setting ->Version Control ->Git
- 配置github:
Setting -> Version Control ->Github
- 建立遠程倉庫並提交代碼:
Vcs ->import into version control ->Share Project on Github
二、接口用例編寫:
1.被測試的接口:
- 登錄;
- 獲取個人信息;
- 獲取商品信息;
- 添加到購物車;
- 保存訂單;
2. 使用excel編寫測試用例:
2.1 表結構:
- 用例ID;
- 模塊;
- 接口名稱;
- 請求URL;
- 前置條件;
- 請求類型;
- 請求參數類型
- 請求參數;
- 預期結果;
- 實際結果;
- 備註;
三、Requests使用:
1. 介紹及使用:
- 介紹:流行的接口http(s)請求工具, 功能強大,簡單方便,容易上手;
- 官網: http://cn.python-requests.org/zh_CN/latest/
2. 用例代碼編寫:
# coding=utf-8
import requests
r = requests.get('http://www.baidu.com')
print(r) # <Response [200]>
- 請求返回介紹:
屬性/方法 | 說明 |
---|---|
r.status_code | 響應狀態碼 |
r.content | 字節方式的響應體,會自動解碼gzip和deflate壓縮 |
r.headers | 以字典對象存儲服務器響應頭,若鍵不存在則返回None |
r.json() | Requests中內置的JSON |
r.url | 獲取url |
r.encoding | 編碼格式 |
r.cookies | 獲取cookie |
r.raw | 獲取原始響應體 |
r.text | 字符串方式的響應體,會自動根據響應頭部的字符編碼進行編碼 |
r.raise_for_status() | 失敗請求(非200響應)拋出異常 |
3. 方法封裝:
- 1.創建封裝方法
- 2.發送requests請求
- 3.獲取結果相應內容
- 4.內容存儲到字典
- 5.字典返回
utils/Request.py
import requests
class Request():
def __init__(self,url):
self.url = url
# 定義公共方法:
def request_api(self, uri, method='get', data=None,json=None, headers=None):
if method == 'get':
r = requests.get(self.url+uri, data=data, json=json, headers=headers)
elif method == 'post':
r = requests.post(self.url+uri, data=data, json=json, headers=headers)
code = r.status_code
try:
body = r.json()
except:
body = r.text
res = {
'code':code,
'body':body
}
return res
# 重構get方法
def get(self, uri,**kwargs):
return self.request_api(uri,**kwargs)
def post(self, uri,**kwargs):
return self.request_api(uri,method='post',**kwargs)
四、配置文件:
使用YAML語言編寫配置文件
1. yaml的介紹與安裝:
1.1 Yaml介紹:
- yaml是一種所有編程語言可用的友好的數據序列化標準, 語法和其他高階語言類似,並且可以簡單表達字典、列表和其他基本數據類型的形態.
- yaml格式作爲文件的配置格式:
- yaml支持註釋;
- 不必強求逗號、括號等符號;
- 通過縮進來區分,視覺上清晰
- yaml官網: https://yaml.org/
1.2 Yaml安裝:
pip install PyYaml
1.3 快速體驗:
- 字典:字典裏的鍵值對用’:'分隔;
data.yml
name: "test_yaml"
result: "success"
import yaml
with open('./data.yml', 'r') as f:
r = yaml.safe_load(f)
print(r)
# {'name': 'test_yaml', 'result': 'success'}
2. 基本操作:
2.1 字典:
- 字典裏的鍵值對用’:'分隔;
- 字典直接寫key: value, 每個鍵值對佔一行;
- key: 後要跟空格
2.2 列表:
- 一組按序列排列的值(簡稱"序列或列表");
- 數組前加有"-"符號,符號與值之間需要用空格分隔;
- 12
- 32
- 33
# [12, 32, 33]
2.3 相互嵌套:
- 字典嵌套字典:
person1:
name: xiaoming
age: 18
person2:
name: xiaohong
age: 16
# person1': {'name': 'xiaoming', 'age': 18}, 'person2': {'name': 'xiaohong', 'age': 16}}
- 字典嵌套列表:
person:
- "a"
- "b"
- c
# {'person': ['a', 'b', 'c']}
- 列表嵌套列表:
-
- 1
- 2
- 3
- "b"
- c
-
- 6
- 7
- 8
# [[1, 2, 3], 'b', 'c', [6, 7, 8]]
- 列表嵌套字典:
-
name: xiaoming
age: 18
- 2
- 3
-
name: xiaohong
age: 16
# [{'name': 'xiaoming', 'age': 18}, 2, 3, {'name': 'xiaohong', 'age': 16}]
3.讀取文件:
3.1 單個文件:
- 中文亂碼: open指定encoding*
data.yml
name: "test_yaml"
result: "success"
import yaml
with open('./data.yml', 'r') as f:
r = yaml.safe_load(f)
print(r)
# {'name': 'test_yaml', 'result': 'success'}
3.2 多個文件:
data.yml
---
name: "test_yaml"
result: "success"
---
# 用'---'分隔,說明是多個文檔
"用戶名稱1": "test123"
"密碼":"123456"
import yaml
with open('./data.yml', 'r') as f:
r = yaml.safe_load_all(f)
for i in r:
print(r)
# {'name': 'test_yaml', 'result': 'success'}
# {"用戶名稱1": "test123","密碼":"123456"}
4.配置文件設置:
- yaml封裝:
./utils/YamlUtil.py
# coding=utf-8
# 1. 創建類
# 2. 初始化,文件是否存在
# 3. yaml讀取
import os
import yaml
class YamlReader():
def __init__(self, yaml_p):
if os.path.exists(yaml_p):
self.yaml_p = yaml_p
else:
raise FileNotFoundError("文件不存在")
self._data = None
self._data_all = None
def data(self):
# 讀取單個文檔
if not self._data:
with open(self.yaml_p, 'r') as f:
self._data = yaml.safe_load(f)
return self._data
def data_all(self):
# 讀取單個文檔
if not self._data:
with open(self.yaml_p, 'r') as f:
self._data_all = list(yaml.safe_load_all(f))
return self._data_all
- 配置文件conf.yaml
./config/conf.yml
BASE:
test:
url: "http://211.103.136.242:8064"
./config/Conf.py
# coding=utf-8
import os
from utils.YamlUtil import YamlReader
# 1. 獲取項目基本目錄
# 1.2 獲取當前項目的絕對路徑
current = os.path.abspath(__file__)
BASE_DIR = os.path.dirname(os.path.dirname(current))
print(current, BASE_DIR)
# 1.3 定義config目錄的路徑
_config_path = BASE_DIR + os.sep + "config"
def get_config_path():
return _config_path
# 1.4 定義conf.yml的文件路徑
_config_file = _config_path + os.sep + 'conf.yml'
def get_config_file():
return _config_file
# 2. 讀取配置文件
class ConfigYaml():
def __init__(self):
self.config = YamlReader(get_config_file()).data()
# 獲取需要的信息
def get_config_url(self):
return self.config['BASE']['test']['url']
if __name__ == "__main__":
conf_read = ConfigYaml()
print(conf_read)
-
基本目錄配置:
-
配置文件讀取及使用:
五、日誌文件:
1.介紹:
1.1 簡介:
logging模塊是python內置的標準模塊,主要用於輸出運行日誌, 可以設置輸出日誌等級、日誌保存路徑等.
1.2 快速使用:
log_demo.py
# coding=utf-8
# 1.導入logging包
import logging
# 2.設置配置信息
logging.basicConfig(level=logging.INFO, format="%(asctime)s-%(name)s-%(levelname)s-%(message)s")
# 3. 定義日誌名稱:get_logger
logger = logging.getLogger('log_demo')
# 4. info,debug
logger.info('info')
logger.debug('debug')
logger.warning('warning')
# 2019-11-20 23:06:09,957-log_demo-INFO-info
# 2019-11-20 23:06:09,958-log_demo-WARNING-warning
2.基本使用:
2.1 日誌輸出到控制檯或文件:
-
- 設置logger名稱
-
- 設置log級別
-
- 創建handler, 用於輸出控制檯或寫入文件
-
- 設置日誌級別
-
- 定義handler的輸出格式
-
- 添加handler
# coding=utf-8
import logging
# 1. 設置logger名稱
logger = logging.getLogger('log_file_demo')
# 2. 設置log級別
logger.setLevel(logging.INFO)
# 3. 創建handler, 用於輸出控制檯或寫入文件
# 輸出到控制檯
fh_stream = logging.StreamHandler()
# 寫入文件
fh_file = logging.FileHandler('./test.log')
# 4. 設置日誌級別
fh_stream.setLevel(logging.INFO)
fh_file.setLevel(logging.INFO)
# 5. 定義handler的輸出格式
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
fh_stream.setFormatter(formatter)
fh_file.setFormatter(formatter)
# 6. 添加handler
logger.addHandler(fh_stream)
logger.addHandler(fh_file)
# 7. 運行
logger.info('this is a info')
logger.debug('this is a debug')
# 2019-11-20 23:29:13,977 log_file_demo INFO this is a info
# 因爲debug的級別小於info,所以不輸出debug
2.2 log級別:
2.3 輸出格式:
Format格式說明:
格式 | 說明 |
---|---|
%(levelno)s | 打印日誌級別的數值 |
%(levelname)s | 打印日誌級別名稱 |
%(pathname)s | 打印當前執行程序的路徑,其實是sys.argv[0] |
%(filename)s | 打印當前執行程序名 |
%(funcName)s | 打印日誌的當前函數 |
%(lineno)d | 打印誒之的當前行號 |
%(asctime)s | 打印日誌的時間 |
%(thread)d | 打印線程ID |
%(threadName)s | 打印線程名稱 |
%(process)d | 打印線程ID |
%(message)s | 打印日誌信息 |
3.封裝工具類:
3.1 封裝Log工具類:
utils/LogUtil.py
# coding=utf-8
# 封裝工具類
# 1.創建類
# 2.定義參數
# 輸出文件名稱,Loggername,日誌級別
# 3.編寫輸出到控制檯或文件
import logging
log_l = {
"info": logging.INFO,
"debug": logging.DEBUG,
"warning": logging.WARNING,
"error": logging.ERROR
}
class logger():
def __init__(self, log_file, log_name, log_level):
self.log_file = log_file # 擴展名, 配置文件
self.log_name = log_name #
self.log_level = log_level
# 設置log名稱
self.logger = logging.getLogger(self.log_nam)
# 設置log級別
self.logger.setLevel(log_l[self.log_level])
# 判斷handler是否存在
if not self.logger.handlers:
# 輸出到控制檯
fh_stream = logging.StreamHandler()
fh_stream.setLevel(log_l[self.log_level])
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
fh_stream.setFormatter(formatter)
# 輸出到文件
fh_file = logging.FileHandler(self.log_file)
fh_file.setLevel(log_l[self.log_level])
fh_file.setFormatter(formatter)
# 添加到handler
self.logger.addHandler(fh_stream)
self.logger.addHandler(fh_file)
3.2 重構配置文件:
config/conf.yml
BASE:
# log等級
log_level: 'debug'
# 擴展名
log_extension: '.log'
test:
url: "http://211.103.136.242:8064"
3.3 日誌工具類應用
config/Conf.py
# coding=utf-8
import os
from utils.YamlUtil import YamlReader
# 1. 獲取項目基本目錄
# 1.2 獲取當前項目的絕對路徑
current = os.path.abspath(__file__)
BASE_DIR = os.path.dirname(os.path.dirname(current))
print(current, BASE_DIR)
# 1.3 定義config目錄的路徑
_config_path =
# 1.4 定義conf.yml的文件路徑
_config_file = _config_path + os.sep + 'conf.yml'
# 定義logs文件路徑
_log_path = BASE_DIR + os.sep + "logs"
def get_config_path():
return _config_path
def get_config_file():
return _config_file
def get_log_path():
"""
獲取log文件路徑
"""
return _log_path
# 2. 讀取配置文件
class ConfigYaml():
def __init__(self):
self.config = YamlReader(get_config_file()).data()
# 獲取需要的信息
def get_config_url(self):
return self.config['BASE']['test']['url']
def get_conf_log(self):
"""
獲取日誌級別
"""
return self.config['BASE_DIR']['log_level']
def get_conf_log_extension(self):
return self.config['BASE_DIR']['log_extension']
if __name__ == "__main__":
conf_read = ConfigYaml()
print(conf_read)
print(conf_read.get_conf_log())
print(conf_read.get_conf_log_extension())
utils/LogUtil.py
# 1.初始化參數數據
# 日誌文件名稱
log_path = Conf.get_log_path()
current_time = datetime.datetime.now().strftime('%Y-%m-%d')
log_extension = ConfigYaml().get_conf_log_extension()
logfile = os.path.join(log_path,current_time+log_extension)
print(logfile)
# 日誌文件級別
loglevel = ConfigYaml().get_conf_log()
print(loglevel)
# 2. 對外方法: 初始化log工具類, 提供其他類使用
def my_log(log_name = __file__):
return Logger(log_file=logfile, log_name=log_name, log_level=loglevel)
if __name__ == "__main__":
my_log().debug("this is a debug")
六、pytesy框架:
1.安裝與入門:
1.1 介紹:
- 簡單靈活;
- 容易上手;
- 文檔豐富;
- 支持參數化
2.基礎使用:
#coding=utf-8
# 1. 創建簡單的測試方法
# 2. pytest運行
# 2.1 idea中直接執行
# 2.2 命令行執行
import pytest
# 創建普通的方法
def func(x):
return x+1
# 創建pytest斷言的方法
def test_a():
print("---test_a---")
assert func(3) == 5 # 斷言失敗
def test_b():
print('---test_b---')
assert func(3) == 4 # 斷言成功
# 代碼直接執行
if __name__ == "__main__":
pytest.main(["pytest_demo.py"])
使用命令行直接執行測試腳本:
pytest pytest_demo.py
2.1 函數級別方法:
- 運行於測試方法的始末;
- 運行一次測試函數會運行一次setup和teardown;
#coding=utf-8
"""
1.定義類;
2.創建測試方法test開頭
3.創建setup, teardown
4.運行查看結果
"""
import pytest
class TestFcun():
def test_a(self):
print('test_a')
def test_b(self):
print('test_b')
def setup(self):
print('------setup------')
def teardown(self):
print('------teardown------')
if __name__ == "__main__":
pytest.main(['-s', 'pytest_func.py'])
"""
PS E:\學習\測試\InterAutoTest_W\testcase\t_pytest> python pytest_func.py
================================== test session starts ================================
platform win32 -- Python 3.7.3, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
rootdir: E:\學習\測試\InterAutoTest_W, inifile: pytest.ini
plugins: remotedata-0.3.1, openfiles-0.3.2, doctestplus-0.3.0, arraydiff-0.3
collected 2 items
pytest_func.py ------setup------
test_a
.------teardown------
------setup------
test_b
.------teardown------
================= 2 passed in 0.08 seconds ==================
"""
2.2 類級別方法:
- 運行於測試類的始末;
- 一個測試內只運行一次setup_class和teardown_class,不關心測試類內有多少測試函數
#coding=utf-8
"""
1.定義類;
2.創建測試方法test開頭
3.創建setup_class, teardown_class
4.運行查看結果
"""
import pytest
class TestClass():
def test_a(self):
print('test_a')
def test_b(self):
print('test_b')
def setup_class(self):
print('------setup_class------')
def teardown_class(self):
print('------teardown_class------')
if __name__ == "__main__":
pytest.main(['-s', 'pytest_class.py'])
"""
PS E:\學習\測試\InterAutoTest_W\testcase\t_pytest> python pytest_class.py
=============== test session starts =================
platform win32 -- Python 3.7.3, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
rootdir: E:\學習\測試\InterAutoTest_W, inifile: pytest.ini
plugins: remotedata-0.3.1, openfiles-0.3.2, doctestplus-0.3.0, arraydiff-0.3
collected 2 items
pytest_class.py ------setup_class------
test_a
.test_b
.------teardown_class------
============================ 2 passed in 0.04 seconds ==============================
"""
3. 常用插件:
- 常用插件: https://plugincompat.herokuapp.com
3.1 測試報告:
- 應用場景:
- 自動化測試腳本最終是通過還是不通過,需要通過測試報告進行提現.
- 安裝:
pip install pytest-html
- 使用
- 在配置文件中的命令行參數中增加 --html=用戶路徑/report.html
./pytest.ini
[pytest]
addopts = --html=./report/report.html
執行測試腳本,生成測試腳本
3.2 失敗重試:
- 應用場景:
- 當失敗後嘗試再次運行
- 安裝:
pip install pytest-rerunfailures
- 使用:
- 在配置文件中的命令行參數中增加 --reruns n (n表示重試的次數)
- 如果期望加上出錯重試等待的時間, --rerun-delay
./pytest.ini
[pytest]
addopts = --html=./report/report.html --reruns 3 --reruns-delay=2
# 使用裝飾器,控制單個測試用例的運行情況
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_b():
print('---test_b---')
assert func(3) == 4 # 斷言成功
在單個測試用例中設置運行控制,則不需要在配置文件中進行配置.
4. 數據參數化:
4.1 傳入單個參數:
@pytest.mark.parametrize(argnames, argvalues)
- argnames: 參數名;
- argvalues:參數對應值,類型必須爲可迭代類型,一般爲list
# coding=utf-8
"""
1. 創建類和測試方法
2. 創建數據
3. 創建參數化
4.運行
"""
import pytest
class TestClass():
data_list = ['xiaoming', 'xiaohong']
@pytest.mark.parametrize('name',data_list)
def test_a(self, name):
print('test_a')
print(name)
assert 1
if __name__ == "__main__":
pytest.main(['-s','pytest_one.py'])
'''
PS E:\學習\測試\InterAutoTest_W\testcase\t_pytest> python .\pytest_one.py
============== test session starts =================================
platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1
rootdir: E:\學習\測試\InterAutoTest_W, inifile: pytest.ini
plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0
collected 2 items
pytest_one.py test_a
xiaoming
.test_a
xiaohong
.
--------- generated html file: file://E:\學習\測試\InterAutoTest_W\testcase\t_pytest\report\report.html ---------
=================== 2 passed in 0.05s =================
'''
4.2 傳入多個參數:
@pytest.mark.parametrize((‘參數1’,‘參數2’), ([參數1_list], [參數2_list]))
- list的每個元素都是一個元組,元組裏的每個元素和參數是按順序一一對應的
# coding=utf-8
"""
1. 創建類和測試方法
2. 創建數據
3. 創建參數化
4.運行
"""
import pytest
class TestClass():
data_list = [('xiaoming', '12345'),('xiaohong', '56789')]
@pytest.mark.parametrize(('name','psw'),data_list)
def test_a(self, name,psw):
print('test_a')
print(name,psw)
assert 1
if __name__ == "__main__":
pytest.main(['-s','pytest_more.py'])
'''
PS E:\學習\測試\InterAutoTest_W\testcase\t_pytest> python .\pytest_more.py
======================= test session starts ======================
platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1
rootdir: E:\學習\測試\InterAutoTest_W, inifile: pytest.ini
plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0
collected 2 items
pytest_more.py test_a
xiaoming 12345
.test_a
xiaohong 56789
.
------- generated html file: file://E:\學習\測試\InterAutoTest_W\testcase\t_pytest\report\report.html ----------------
===================== 2 passed in 0.06s ============================
'''
5.應用接口用例:
- 登錄;
- 個人信息;
- 獲取商品信息;
- 購物車;
- 訂單;
5.1 pytest運行原則:
5.2.1 默認規則:
- 在不指定運行目錄,運行文件,運行函數等參數的默認情況下, pytest會執行當前目錄下所有的以test爲前綴(test*.py)或以_test爲後綴(_test.py)的文件中以test爲前綴的函數.
# coding=utf-8
"""
1. 根據默認運行原則,調整py文件命名,函數命名
2. pytest.main()運行,或者命令行直接運行
"""
import sys
sys.path.append('../')
import pytest
import requests
from utils.RequestsUtil import Request, r_get,r_post
request = Request(url='http://211.103.136.242:8064')
def test_login():
"""
登錄
"""
url = 'http://211.103.136.242:8064/authorizations/'
data = {'username':'python', 'password':'12345678'}
r = r_post(url, json=data)
print(r)
"""
PS E:\學習\測試\InterAutoTest_W\testcase> python .\test_mall.py
============================ test session starts ========================================
platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1
rootdir: E:\學習\測試\InterAutoTest_W, inifile: pytest.ini
plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0
collected 1 item
test_mall.py {'code': 200, 'body': {'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzQ4NTAyMzIsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.GLoT8ncQu9Pd74x0EoVjiXdnKED6JsB4WkasS8d6aPw', 'username': 'python', 'user_id': 1}}
.
--------- generated html file: file://E:\學習\測試\InterAutoTest_W\testcase\report\report.html -------------
========================= 1 passed in 2.34s =========================
"""
5.2.2 自定義規則運行:
使用pytest.ini文件配置:
./pytest.ini
addopts = -s
# 運行參數
testpaths = testcases
# 當前目錄下的script文件夾, -可自定義
python_files = test_*.py
# 當前目錄下的script文件夾下,以test_開頭,以.py結尾的所有文件
python_classes = Test_*
# 當前目錄下的script文件夾下,以test_開頭,以.py結尾的所有文件, 以Test_開頭的類 -可自定義
python_functions = test_*
# 當前目錄下的script文件夾下,以test_開頭,以.py結尾的所有文件, 以Test_開頭的類, 以test_開頭的方法 -可自定義
七、結果斷言:
1.結果斷言驗證:
1.1 常用斷言:
- 介紹:
- 斷言是自動化最終的目的,一個用例沒有斷言,就失去了自動化測試的意義了;
- 斷言用到的是assert關鍵字;
- 預期結果和實際結果做對比;
- 常用斷言:
情況 | 語句 |
---|---|
判斷x爲真 | assert x |
判斷x不爲真 | assert not x |
判斷b包含a | assert a in b |
判斷a等於b | assert a == b |
判斷a不等於b | assert a != b |
1.2斷言應用接口用例:
- 返回狀態碼:
- 結果驗證:
1.3斷言封裝
./utils/AssertUtil.py
# coding=utf-8
"""
1. 定義封裝類;
2. 初始化數據,日誌
3. code相等
4. body相等
5.body包含
"""
import json
from utils.LogUItil import my_log
class AssertUtil():
def __init__(self):
self.log = my_log('AssertUtil')
def assert_code(self, code, expected_code):
"""
驗證返回狀態碼
"""
try:
assert int(code) == int(expected_code)
return True
except Exception as e:
self.log.error(f'code error, code is {code}, expected_code is {expected_code}')
raise
def assert_body(self, body, expected_body):
"""
驗證返回結果內容相等
"""
try:
assert body == expected_body
return True
except Exception as e:
self.log.error(f'body error, body is {body}, expected_body is {expected_body}')
raise
def assert_in_body(self, body, expected_body):
"""
驗證返回結果是否包含期望的結果
"""
try:
assert json.dumps(body) in json.dumps(expected_body)
return True
except Exception as e:
self.log.error(f'body error, body not in expected_body, body is {body}, expected_body is {expected_body}')
raise
2. 數據庫結果斷言驗證:
2.1 pymysql安裝及簡單實用:
- 安裝:
pip install pymysql
- 簡單實用:
# coding=utf-8
"""
1. 導入pymysql
2. 鏈接database
3. 獲取執行sql的光標對象
4. 執行sql
5. 關閉對象
"""
import pymysql
cnn = pymysql.connect(
host='211.103.136.242',
port=7090,
user='test',
password='test123456',
database='meiduo',
charset='utf8',
)
with cnn.cursor() as cursor:
sql_str = 'select * from tb_users'
cursor.execute(sql_str)
res = cursor.fetchall()
print(res)
cnn.close()
2.2 工具類封裝及使用:
- pymysql工具類封裝:
./utils/Mysqlutil.py
# coding=utf-8
"""
1. 創建封裝類
2.初始化數據,連接數據庫,光標對象
3. 創建查詢,執行方法
4. 關閉對象
"""
import pymysql
import functools
from utils.LogUItil import my_log
class Mysql():
def __init__(self,host,user,password,database,port=3306,charset='utf8'):
self.cnn = pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset=charset
)
self.cursor = self.cnn.cursor()
self.log = my_log()
def __del__(self):
try:
self.cnn.close()
except Exception as e:
pass
try:
self.cursor.close()
except Exception as e:
pass
def fetch_one(self, sql):
"""
查詢一個對象
"""
self.cursor.execute(sql)
return self.fetchone()
def fetch_all(self, sql):
"""
查詢全部對象
"""
self.cursor.execute(sql)
return self.fetchall()
def exec(self,sql):
"""
執行操作
"""
try:
self.cursor.execute(sql)
self.cursor.commit()
except Exception as e:
self.cnn.rollback()
self.log.error(e)
return False
return True
if __name__ == "__main__":
mysql = Mysql(
host='211.103.136.242',
port=7090,
user='test',
password='test123456',
database='meiduo',
charset='utf8',
)
res = mysql.fetch_all('select * from tb_uders')
print(res)
- 配置文件:
# ./config/db_conf.yml
db_1:
db_host: "211.103.136.242"
db_port: 7090
db_user: "test"
db_password: "test123456"
db_database: "meiduo"
db_charset: "utf8"
./config/Conf.py
"""
1. 創建db_conf.yml
2. 編寫數據庫基本信息
3. 重構Conf.py
4. 執行
"""
...
# 定義db_conf.yml文件路徑
_db_config_file = _config_path + os.sep + 'db_conf.yml'
def get_db_conf_file():
"""
獲取db_conf文件路徑
"""
return _db_config_file
# 2. 讀取配置文件
class ConfigYaml():
def __init__(self):
self.config = YamlReader(get_config_file()).data()
self.db_config = YamlReader(get_db_conf_file()).data()
def get_db_conf_info(self, db_alias):
"""
根據db_alias獲取數據庫相關參數
"""
return self.db_config[db_alias]
if __name__ == "__main__":
conf_read = ConfigYaml()
......
print(conf_read.get_db_conf_info('db_1'))
- 數據庫結果驗證:
-
- 初始化數據庫信息,Base.py, init_db
-
- 接口用例返回結果內容進數據庫驗證
-
# ./common/Base.py
# coding=utf-8
# 1.定義一個方法init_db
# 2.初始化數據庫信息, 通過配置文件來完成
# 3.初始化mysql對象
from config.Conf import ConfigYaml
from utils.MysqlUtil import Mysql
def init_db(db_alias):
db_init = ConfigYaml().get_db_conf_info(db_alias)
host = db_init.get('host', '127.0.0.1')
port = int(db_init.get('port', 3306))
user = db_init.get('user')
database = db_init.get('database')
password = db_init.get('password')
charset = db_init.get('charset', 'utf8')
conn = Mysql(host=host,port=port,user=user,password=password,database=database,charset=charset)
return conn
if __name__ == "__main__":
conn = init_db('db_1')
print(conn)
八、數據驅動:
1. yaml數據驅動:
1.1 yaml測試用例:
#./data/testlogin.yml
# 登錄的測試用例:
# 測試名稱:
# url地址
# data
# 期望結果
---
'case_name':"登陸成功用例"
'url': 'http://211.103.136.242:8064/authorizations/'
'data':
'username': "python"
'password': "12345678"
'expect': "'username': 'python', 'user_id': 1"
---
'case_name':"登陸失敗用例"
'url': 'http://211.103.136.242:8064/authorizations/'
'data':
'username': "test123456"
'password': "1231111"
'expect': "'username': 'python', 'user_id': 1"
1.2 參數化:
# ./testcase/test_login.py
# coding=utf-8
# 1. 獲取測試用例的列表
# 獲取testlogin.yml文件路徑
# 使用工具類來讀取多個文檔的內容
# 2. 參數化執行測試用例
import os
import sys
sys.path.append("../")
import pytest
from config import Conf
from config.Conf import ConfigYaml
from utils.YamlUtil import YamlReader
from utils.RequestsUtil import Request
test_file = os.path.join(Conf.get_data_path(), 'testlogin.yml')
print(test_file)
data_list = YamlReader(test_file).data_all()
print(data_list)
@pytest.mark.parametrize("login", data_list)
def test_yaml(login):
"""
執行測試用例
"""
uri = login['url']
print(uri)
data = login['data']
print(data)
request = Request(ConfigYaml().get_config_url())
res = request.post(uri, json=data)
print(res)
if __name__ == "__main__":
pytest.main(['test_login.py'])
2. Excel數據驅動
2.1 excel用例設計:
(./data/testdata.xlsx)
2.2 excel讀取:
# coding=utf-8
"""
1. 導入包, xlrd(python自帶)
2. 創建workbook對象
3. sheet對象
4. 獲取行數和列數
5. 讀取每行的內容
6. 讀取每列的內容
7. 讀取固定列的內容
"""
import xlrd
book = xlrd.open_workbook('./testdata.xlsx')
# 獲取表的兩種方式:
# 索引
# sheet = book.sheet_by_index(0)
# 名稱
sheet = book.sheet_by_name('美多商城接口測試')
rows = sheet.nrows # 行數
cols = sheet.ncols # 列數
print(f"rows:{rows}, cols:{cols}")
# 獲取每行數據
for r in range(rows):
r_values = sheet.row_values(r)
print(r_values)
# 獲取每列數據
for c in range(cols):
c_values = sheet.col_values(c)
print(c_values)
# 讀取固定列的內容
v = sheet.cell(1,2)
print(v)
2.3 封裝excel工具類:
# ./utils/ExcelUtil.py
# coding=utf-8
# 目的: 參數化, pytest list
# 1. 驗證文件是否存在,存在讀取,不存在錯報
# 2. 讀取sheet方式, 名稱,索引
# 3. 讀取sheet內容
# 返回list, 字典
# 格式: [{'a':"a1",'b':" b1"}, {'a':"a2",'b':"b2"}]
# 4. 結果返回
import os
import xlrd
# 自定義異常
class SheetTypeError(object):
pass
class ExcelReader():
def __init__(self, excel_file, sheet_by):
if os.path.exists(excel_file):
self.excel_file = excel_file
self.sheet_by = sheet_by
self.data_list = []
else:
raise FileNotFoundError("文件不存在")
def data(self):
if self.data_list:
return self.data_list
workbook = xlrd.open_workbook(self.excel_file)
if type(self.sheet_by) not in [str,int]:
raise SheetTypeError("參數錯誤")
elif type(self.sheet_by) == int:
sheet = workbook.sheet_by_index(self.sheet_by)
elif type(self.sheet_by) == str:
sheet = workbook.sheet_by_name(self.sheet_by)
# 獲取首行信息
title = sheet.row_values(0)
for r in range(1, sheet.nrows):
self.data_list.append(dict(zip(title,sheet.row_values(r))))
# print(self.data_list)
return self.data_list
if __name__ == "__main__":
excel_reader = ExcelReader('../data/testdata.xlsx',"美多商城接口測試")
print(excel_reader.data())
2.4 excel參數化運行:
- 獲取是否運行;
- 參數化;
- 結果斷言;
# ./common/ExcelConfig.py
# coding=utf-8
# 定義類
# 定義列屬性
# 定義excel的映射
class DataConfig():
# 用例屬性:
case_id = "用例ID"
case_model = "模塊"
case_name = "接口名稱"
url = "請求URL"
pre_exec = "前置條件"
method = "請求類型"
params_type = "請求參數類型"
params = "請求參數"
expect_result = "預期結果"
actual_result = "實際結果"
beizhu = "備註"
is_run = "是否運行"
headers = "headers"
cookies = "cookies"
code = "status_code"
db_verify = "數據庫驗證"
# ./config/ExcelData.py
# coding=utf-8
# 1. 使用excel工具類, 獲取結果list
# 2. 列"是否運行內容", y
# 3. 保存要執行結果, 放到新的列表中
import sys
sys.path.append('../')
from utils.ExcelUtil import ExcelReader
from common.ExcelConfig import DataConfig
class Data():
def __init__(self, excel_file, sheet_by):
self.reader = ExcelReader(excel_file, sheet_by)
self.run_list = []
def get_run_data(self):
"""
根據"是否運行"列,獲取執行測試用例
"""
for line in self.reader.data():
if str(line[DataConfig().is_run]).lower() == 'y':
self.run_list.append(line)
return self.run_list
2,4 動態的headers請求
-
- 判斷header是否存在? json轉義: 無需
-
- 增加Headers
-
- 增加cookies
-
- 發送請求
2.5 動態關聯:
-
- 驗證前置條件;
-
- 找到執行用例;
-
- 發送請求,獲取前置用例結果;
- 發送獲取前置用例;
- 數據初始化, 重構get/post
-
- 替換headers變量;
- 驗證請求中師傅含有, 返回內容
- 根據內容token,查詢前置條件用例返回結果
- 根據變量結果內容,替換
-
- 發送請求
2.6 斷言驗證:
- 狀態碼;
- 返回結果內容;
- 數據庫相關結果的驗證
-
- 初始化數據庫;
-
- 查詢sql語句;
-
- 獲取數據庫結果的key;
-
- 根據key獲取數據庫結果和接口結果
-
- 驗證
-
九、Allure報告:
1.快速入門:
1.1 allure安裝:
- python插件:
- 命令行安裝:
pip install allure-pytest
- 源代碼安裝:
htpps://pypi.org/project/allure-pytest
- allure工具:
- 下載:
https://bintray.com/qameta/generic/allure2
https://github.com/allure-framework/allure2 - 前置條件:
- 已部署java環境
- 解壓並配置系統環境變量;
- 下載:
1.2 allure使用:
- 配置pytest.ini
[pytest]
# addopts = -s --html=./report/report.html --reruns 3 --reruns-delay=2
# allure setting
addopts = -s --allure ./report/report.html
...
- 添加allure代碼;
- 運行;
- allure工具生成html報告;
allure generate {result_path} -o {report_path}
or
allure generate {result_path} -o {report_path} --clean
2. allure詳解:
註解 | 說明 |
---|---|
Title | 可以自定義用例標題, 標題默認函數名 |
Description | 測試用例的詳細說明 |
Feature | 定義功能模塊, 往下是Story |
Story | 定義用戶故事 |
Severity | 定義用例級別,主要有BLOCKER(攔截器), CRITICAL(關鍵), MINOR(次要), NORMAL(正常), TRIVIAL(瑣碎)等幾種類型,默認是NORMAL |
Allure.dynamic | 動態設置相關配置 |
# coding=utf-8
import pytest
import allure
@allure.feature('接口測試, 這是一個一級標籤')
class TestAllure():
# 定義測試方法
@allure.title('測試用例標題1')
@allure.description('執行測試用例1的結果是:test_1')
@allure.stroy("這是一個二級標籤:test1")
@allure.severity(allure.severity.CRITICAL)
def test_1(self):
print("test_1")
@allure.title('測試用例標題2')
@allure.description('執行測試用例2的結果是:test_2')
@allure.stroy("這是一個二級標籤:test1")
@allure.severity(allure.severity.BLOCKER)
def test_2(self):
print("test_2")
@allure.title('測試用例標題3')
@allure.description('執行測試用例3的結果是:test_3')
@allure.stroy("這是一個二級標籤:test3")
def test_3(self):
print("test_3")
@pytest.mark.parametrize("case",['case1', 'case2', 'case3'])
def test_4(self, case):
print(f"test4: {case}")
allure.dynamic.title(case)
if __name__ == "__main__":
pytest.main(['allure_demo.py'])
3. Allure應用:
3.1 應用測試用例:
1) 區分層級:
- sheet名稱 --> feature一級標籤
- 模塊 --> story 二級標籤
- 用例id+接口名稱 --> title
- 請求url,請求類型,期望結果, 實際結果描述
# ./tesecase/test_excel_case.py
...
class TestExcel():
...
@pytest.mark.parametrize('case', run_list)
def test_run(self,case):
...
print(f"執行測試用例:{res}")
# allure
allure.dynamic.feature(sheet_name)
allure.dynamic.story(case_model)
allure.dynamic.title(case_id + case_name)
desc = f"url:{url}<br> 請求方法:{method}<br> 期望結果:{expect_result}<br> 實際結果:{res}"
allure.dynamic.description(desc)
# 斷言驗證
...
2) 自動生成測試報告:
不用再手動執行生成allure的html報告
- subprocess介紹:
- 允許生產新的進程,並且可以把輸入,輸出,錯誤直接連接到管道,最後獲取結果
- 官方網站:
- https://docs.python.org/3.8/library/subprocess.html
- 方法:
- subprocess.all(): 父進程等待子進程完成, 返回退出信息(returncode, 相當於linux exit code)
- shell=True的用法, 支持命令以字符串的形式傳入
# ./common/Base.py
def allure_report(report_path):
allure_cmd = f"allure generate {report_path}/result -o {report_path}/html --clean"
log.info(f"報告地址:{report_path}")
try:
subprocess.call(allure_cmd, shell=True)
except Exception as e:
log.error(e)
3.2 項目運行:
十、郵件配置:
1. 配置文件設置及郵件封裝:
(略)
2. 郵件運行:
(略)
三、持續集成與docker介紹及配置:
1. jenkins和docker介紹及安裝:
1.1 docker:
1) 介紹:
- 開源免費;
- 方便快速搭建環境;
- 自動化測試和持續集成,發佈;
2)安裝:
- 流程:(基於centos)
-
- 查看內核版本;
-
- 安裝需要的軟件包;
-
- 設置yum源;
-
- 查詢及安裝docker;
-
- 啓用docker, 並加入開機啓動;
-
3) docker拉取鏡像慢:
將鏡像源修改爲阿里雲鏡像源
-
- 編輯docker配置文件:
vi /lib/systemd/system/docker.service
-
- 修改配置內容:
ExecStart=/usr/bin/docker
修改爲:
ExecStart=/usr/bin/docker --registry-mirror=https://u1qbyfsc.mirror.aliyuncs.com
4) docker的基本用法:
常用命令 | 說明 |
---|---|
docker images | 列出本地主機上的鏡像 |
docker ps -a | 查看容器狀態 |
docker start(stop,restart) container-name | 容器啓動(關閉,重啓)命令 |
docker exec | 進入正在後臺執行的容器(參數:-d: 分離模式,在後臺執行; -i: 即使沒有附加也保持STDIN打開; -t: 分配一個僞終端) |
1.2 jenkins:
1) 介紹:
- 開源免費;
- 安裝配置簡單;
- 跨平臺,支持所有平臺;
- web形式的可視化管理頁面;
- 分佈式構建;
- 豐富的插件支持;
2) 安裝:
系統: centos; 容器: docker
-
- 選擇docker版本安裝;
- https://jenkins.io/zh/download/
-
- 安裝;
-
- 查看下載完的鏡像;
-
- 啓動jenkins鏡像;
-
- 瀏覽http://localhost 並等待Unlock Jenkins頁面出現;
-
- 使用後面的步驟設置嚮導完成設置;
3) 啓動jenkins:
docker run -d -p 80:8080 -p 50000:50000 -v jenkins:/var/jenkins_home -v /etc/localtime:/etc/localtime --name jenkins docker.io/jenkins/jenkins:lts
- 啓動參數意義:
參數 | 意義 |
---|---|
-d | 後臺運行 |
-p 80:8080 | 將鏡像的8080端口映射到服務器的80端口 |
-p 50000:50000 | 將鏡像的50000端口映射到服務器的50000端口 |
-v jenkins:/var/jenkins_home | /var/jenkins_home目錄爲jenkins工作目錄,將硬盤上的一個目錄掛載到這個位置, 方便後續更新鏡像後繼續使用原來的工作目錄 |
-v /etc/localtime:/etc/localtime | 讓容器使用和服務器使用同樣的時間設置 |
–name jenkins | 給容器起個名 |
2. Jenkins插件安裝及配置:
2.1 allure:
1)安裝:
-
- 進入jenkins -> 系統管理 -> 管理插件 -> 可選插件
-
- 搜索框輸入"allure" -> 選中點擊"直接安裝" -> 等待安裝完成 -> 重啓jenkins
2) 配置(Allure Commandline):
-
設置路徑:
- 系統設置 -> 全局工具配置 -> Allure Commandline
-
- Allure下載地址:
- https://dl.bintray.com/qameta/generic/io/qameta/allure/2.7.0
-
- Allure安裝:
- 解壓到本地, 配置環境變量即可.
2.2 git:
1)安裝:
git是默認安裝的
2) 配置:
-
- 系統管理 -> 全局工具配置 -> git
-
- 點擊"添加" -> 輸入name,path
3. jenkins持續集成配置及運行:
3.1 general:
1) 創建項目
-
- 進入jenkins主頁;
-
- 點擊"新建";
-
- 輸入項目名稱, 並點擊"構建一個自由風格的軟件項目";
-
- 點擊"確定"
2) 配置項目:
-
- General;
- a. 節點配置:
- 系統管理 -> 節點管理 -> 新建節點
- windows爲例:
- 工作目錄: 工作目錄
- 啓動方式: “通過java web啓動代理”
- b. 選擇"在必要的時候併發構建"
- c. 選擇"限制項目的運行節點" -> 輸入要運行的節點
- 點擊"保存"
-
- 源碼管理;
- a. 選擇"Git";
- b. 輸入git地址;
- c. 添加jenkins憑證認證;
-
- 構建觸發器;
- 第一顆 * 表示分鐘, 取值0~59
- 第二顆 * 表示小時, 取值0~23
- 第三顆 * 表示一個月的第幾天, 取值1~31
- 第四顆 * 表示一週中的第幾天, 取值0~7, 其中0和7代表的都是週日
- 構建觸發器;
-
- 構建配置;
- 增加構建步驟:
- windows:
- Execute Windows batch command
- linux/Mac:
- Execute shell
- windows:
- 編寫run.py
# ./run.py
# coding=utf-8
import os
import pytest
from config import Conf
if __name__ == "__main__":
report_path = Conf._report_path()
pytest.main(['-s', '--alluredir', report_path+'/reslut'])
-
- 報告;
-
- 點擊增加構建後步驟,選擇"Allure Report";
-
- 在path中輸入allure報告所在的目錄名稱;
-
- 郵件;
-
- jenkins -> 系統配置 -> 郵件通知
-
- 設置相關信息;
-
- 構建後的操作 -> 增加構建有的操作步驟 -> E-mail notification -> 接收郵箱
3) 運行項目:
-
自動構建:
-
手動構建:
- jenkins -> 立即構建 -> 點擊進度條 -> 點擊"控制檯輸出" -> 查看報錯信息
4) 查看郵件:
5) 查看報告:
3.2 源碼管理:
3.3 其他配置:
3.4 運行:
4、拓展:
4.1 學習網站:
- Dokcker安裝jenkins:
- https://github.com/jenkinsci/docker/blob/master/README.md
- Docker使用:
- https://www.runoob.com/docker/docker-container-usage.html