directory
-
nonsense
-
function
-
Operation mechanism
-
Known issues
-
Environment and Dependency
-
The directory structure
-
Execution order
-
The config. Ymal display
-
EXcel use case presentation
-
The scripts view
- Request method encapsulation
- Read Excel use case data
- Storage port actual result response
- Processing dependent data logic
- Startup file
-
Run the test
-
The results
-
Thank you
-
The source address
-
update
nonsense
Chat with a few friends and then come out of the product hope to help you learn interface automation testing, welcome you to exchange points out inappropriate place, source code in the end of the article
function
- Implementation: GET/POST request (upload file): theoretically, other DELETE/PUT requests are implemented, and restful interface specifications are supported
- Send E-mail
- Generate allure test report
- Compress the test report file
- Data dependencies
Operation mechanism
- By reading the configuration file, the host address is obtained, the JSONPATH expression of token extraction is obtained, and the JSONPATH expression of the actual response result is extracted to compare with the expected result.
- Read excel use case file data, form a PyTest parameterized use case data, and process data according to each column (token operation, data dependency)
- Token and write: The token can be extracted only when a normal login interface returns token data. If the request is read as a header containing the token, the request does not carry the token if the token has no data
- Data dependency processing, format {” Use Case number “:[” jSONPATH expression 1″, “JSONPATH expression 2″]} read from Excel, through the use case number to obtain the actual response result of the corresponding case (actual response result after sending the request, {” userID “:500, “username”: {” userID “:500, “username”: “Zy7y “}, when the request is sent, the request data is merged to form a new data into the request
- The actual response results are written back to Excel after each request is completed
- Extract the actual response content from the jSONPATH expression configured in the configuration file and compare the data with the expected results in Excel
- Generate test report
- Compress the test report folder
- Send E-mail
Known issues
Code executed interface consumes time longer, not solid (linguistics), frequently to read and write excel (can consider to use a dictionary to save the actual response of each interface, directly from the response values in the dictionary out) the overall code structure optimization unrealized, lead to the ultimate test of time longer, other tools single interface test only need 39 ms, the framework is used in the 101 ms, Consider and frequently read and write use case data results
Environment and Dependency
The name of the | version | role |
---|---|---|
python | 3.7.8 | |
pytest | The 6.0.1 | An underlying unit testing framework for parameterization and automatic execution of use cases |
allure-pytest | 2.8.17 | The Plugin for Allure and PyTest generates test reports for Allure |
jsonpath | 0.82 | Used for response assertion operations |
loguru | 0.54 | log |
PyYAML | 5.3.1 | Read the configuration file in YML/YAML format |
Allure | 2.13.5 | To generate allure test reports, you must install Allure on your machine and configure environment variables |
xlrd | 1.2.0 | Used to read excel case data |
yagmail | 0.11.224 | Send an email when the test is complete |
requests | 2.24.0 | Send the request |
The directory structure
Execution order
Run test_api.py -> read config.yaml(tools.read_config.py) -> read Excel use case file (tools.read_data.py) -> implement parameterization -> whether to rely on data Py sends the request -> test_api.py asserts -> read_data.py writes back the actual response to the use case file (easy to extract data based on dependencies)
The config. Ymal display
server:
test: http://127.0.0.1:8888/api/private/v1/
# Example code using the interface service has been changed to the author's own cloud server deployment. (back-end source from b station: https://www.bilibili.com/video/BV1EE411B7SU?p=10)
dev: http://49.232.203.244:8888/api/private/v1/
# actually respond to jSONPath extraction rule Settings
response_reg:
Extract token jsonPath expression
token: $.data.token
Extract the jSONPATH expression for the actual response and compare it with the expected result data in Excel
response: $.meta
file_path:
case_data: ../data/case_data.xlsx
report_data: ../report/data/
report_generate: ../report/html/
report_zip: ../report/html/apiAutoTestReport.zip
log_path: . /log/ Run log {time}.log
email:
# Sender email
user: 123456.com
# Sender email authorization code
password: ASGCSFSGS
# mailbox host
host: smtp.163.com
contents: After unpacking apiAutoReport.zip(Interface Test Report), use Live installed Server VsCode for the plugin, open the index. HTML folder to view the report
# Recipient email
addressees: ["Recipient MAILBOX 1"."Recipient EMAIL Box 2"."Recipient EMAIL Box 3"]
title: Interface automation test Report (see attachment)
# Attachment address
enclosures: [".. /report/html/apiAutoTestReport.zip",]
Copy the code
EXcel use case presentation
The scripts view
Request method encapsulation
#! /usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: base_requests.py
@ide: PyCharm
@time: 2020/7/31
"""
from test import logger
import requests
class BaseRequest(object) :
def __init__(self) :
pass
# request
def base_requests(self, method, url, parametric_key=None, data=None, file_var=None, file_path=None, header=None) :
""" : Param method: request method: param URL: request URL: Param parametric_key: Get /delete/head/options/ params/POST/PUT /patch/application/json /data :param data: Parameter data, default is None :param file_var: parameter of the file accepted by the interface Keyword :param file_path: param file_path: The address of the file object, a single file directly address: / Users/zy7y/Desktop/vue. Js multiple file formats: ["/Users/zy7y/Desktop/vue. Js ", "/ Users/zy7y/Desktop/Jenkins. War"] : param header: request header: return: return to the response of the json format "" "
session = requests.Session()
if (file_var in [None.' ']) and (file_path in [None.' ']):
files = None
else:
# File not empty operation
if file_path.startswith('[') and file_path.endswith('] '):
file_path_list = eval(file_path)
files = []
# Multiple file uploads
for file_path in file_path_list:
files.append((file_var, (open(file_path, 'rb'))))
else:
# Single file upload
files = {file_var: open(file_path, 'rb')}
if parametric_key == 'params':
res = session.request(method=method, url=url, params=data, headers=header)
elif parametric_key == 'data':
res = session.request(method=method, url=url, data=data, files=files, headers=header)
elif parametric_key == 'json':
res = session.request(method=method, url=url, json=data, files=files, headers=header)
else:
raise ValueError(Get /delete/head/options/ request params, post/ PUT /patch request json (application/json) /data)
logger.info(F 'request method:{method}, request path:{url}, request parameters:{data}, request file:{files}, request header:{header}) ')
return res.json()
Copy the code
Read Excel use case data
#! /usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: read_data.py
@ide: PyCharm
@time: 2020/7/31
"""
import xlrd
from test import logger
class ReadData(object) :
def __init__(self, excel_path) :
self.excel_file = excel_path
self.book = xlrd.open_workbook(self.excel_file)
def get_data(self) :
""" :return: data_list-pytest Parameterizes available data """
data_list = []
table = self.book.sheet_by_index(0)
for norw in range(1, table.nrows):
Is column 4 of each row run
if table.cell_value(norw, 3) = ='no':
continue
value = table.row_values(norw)
value.pop(3)
Value = tuple(value)
value = tuple(value)
logger.info(f'{value}')
data_list.append(value)
return data_list
Copy the code
Storage port actual result response
#! /usr/bin/env/python3
# -*- coding:utf-8 -*-
"" @project: copy of apiAutoTest @author: zy7y @file: save_response.py @ide: PyCharm @time: 2020/8/8 ""
import json
import jsonpath
from test import logger
class SaveResponse(object) :
def __init__(self) :
self.actual_response = {}
Save the actual response
def save_actual_response(self, case_key, case_response) :
""" :param case_key: use case number :param case_response: actual response to application case number :return: """
self.actual_response[case_key] = case_response
logger.info(F 'Current dictionary data{self.actual_response}')
Read dependent data
def read_depend_data(self, depend) :
{"case_001":"['jsonpaht expression 1', 'jsonpaht expression 2']"} :return: ""
depend_dict = {}
depend = json.loads(depend)
for k, v in depend.items():
Get the value extraction expression of the corresponding case number in the dependency
try:
for value in v:
# value : '$.data.id'
Get the actual response to the application case number
actual = self.actual_response[k]
Return the key that depends on the data
d_k = value.split('. ')[-1]
Add to the dependency data dictionary and return
depend_dict[d_k] = jsonpath.jsonpath(actual, value)[0]
except TypeError as e:
logger.error(F 'cannot extract any content from the actual response result using this expression normally, and exceptions are found{e}')
return depend_dict
Copy the code
Processing dependent data logic
#! /usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: data_tearing.py
@ide: PyCharm
@time: 2020/8/10
"""
import json
from json import JSONDecodeError
import jsonpath
from test import logger
class TreatingData(object) :
Handle hader/path path parameter/request data dependency data code
def __init__(self) :
self.no_token_header = {}
self.token_header = {}
def treating_data(self, is_token, parameters, dependent, data, save_response_dict) :
# Use that header
if is_token == ' ':
header = self.no_token_header
else:
header = self.token_header
logger.info(F 'handles data dependent on pre-data:{data}')
# process dependent data data
ifdependent ! =' ':
dependent_data = save_response_dict.read_depend_data(dependent)
logger.debug(F 'depends on the dictionary obtained from data parsing{dependent_data}')
ifdata ! =' ':
Merge to form a new data
dependent_data.update(json.loads(data))
data = dependent_data
logger.info(F 'data has data, depends on data{data}')
else:
# assign to data
data = dependent_data
logger.info(F 'data No data is available{data}')
else:
if data == ' ':
data = None
logger.info(F 'data Indicates that there is no data{data}')
else:
data = json.loads(data)
logger.info(F 'data has data and depends on no data{data}')
# handle the dependency of the Path parameter
# incoming parameters like {" case_002 ":" $. Data. Id} / item / {" case_002 ":" $. Meta. Status "}, split to list
path_list = parameters.split('/')
Get the list length iteration
for i in range(len(path_list)):
# according to
try:
Loads ('2') can be converted to 2
path_dict = json.loads(path_list[i])
except JSONDecodeError as e:
The value of path_list[I] does not change
logger.error(F 'could not convert the dictionary, go to the next check, this rotation does not change:{path_list[i]}.{e}')
# Skip to the next loop
continue
else:
Parse the dictionary for use-case numbers, expressions
logger.info(f'{path_dict}')
Loads (' number ') normal serialization caused by AttributeError
try:
for k, v in path_dict.items():
try:
Try to extract the contents of a field from the corresponding case actual response
path_list[i] = jsonpath.jsonpath(save_response_dict.actual_response[k], v)[0]
except TypeError as e:
logger.error(F 'cannot be extracted. Please check if the expression is supported in the response dictionary.{e}')
except AttributeError as e:
logger.error(F 'type error:{type(path_list[i])}, the value will not be converted{path_list[i]}.{e}')
There are elements in the dictionary that are not STR: use map to convert a list of completed strings
path_list = map(str, path_list)
# Convert a list of strings to characters: 500/item/200
parameters_path_url = "/".join(path_list)
logger.info(F 'path The path of the dependency resolution parameter is{parameters_path_url}')
return data, header, parameters_path_url
Copy the code
Startup file
#! /usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import jsonpath
from test import logger
import pytest
import allure
from api.base_requests import BaseRequest
from tools.data_tearing import TreatingData
from tools.read_config import ReadConfig
from tools.read_data import ReadData
from tools.save_response import SaveResponse
Read configuration file objects
rc = ReadConfig()
base_url = rc.read_serve_config('dev')
token_reg, res_reg = rc.read_response_reg()
case_data_path = rc.read_file_path('case_data')
report_data = rc.read_file_path('report_data')
report_generate = rc.read_file_path('report_generate')
log_path = rc.read_file_path('log_path')
report_zip = rc.read_file_path('report_zip')
email_setting = rc.read_email_setting()
Instantiate the object that holds the response
save_response_dict = SaveResponse()
Read excel data objects
data_list = ReadData(case_data_path).get_data()
# data processing object
treat_data = TreatingData()
Request object
br = BaseRequest()
logger.info(F 'configuration file/Excel data/object instantiation, wait for preconditions to be processed \n\n')
class TestApiAuto(object) :
Start method
def run_test(self) :
import os, shutil
if os.path.exists('.. /report') and os.path.exists('.. /log'):
shutil.rmtree(path='.. /report')
shutil.rmtree(path='.. /log')
Log access path
logger.add(log_path, encoding='utf-8')
pytest.main(args=[f'--alluredir={report_data}'])
os.system(f'allure generate {report_data} -o {report_generate} --clean')
logger.warning('Report generated')
@pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_var,'
'file_path, parameters, dependent,data,expect', data_list)
def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_var, file_path, parameters, dependent, data, expect) :
""" :param case_number: case number: param case_title: Case title: param path: interface path: Param is_token: Token operation: Write the token, read the token, or do not carry the token :param method: Request method: get, POST, PUT, or delete.... : Param parametric_key: entry parameter Keyword: params/data/json :param file_var: parameter name of the file object accepted on the interface: Param file_path: File path, single file instance: /Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py Multiple file example ['/Users/zy7y PycharmProjects/apiAutoTest/test/set p y ', '/ Users/zy7y/PycharmProjects/apiAutoTest/test/test_api. Py '] :param parameters: path (' path ', 'path', 'path') '$.data.id'}, parse the data dictionary id from the actual result response of case_001 (let's say 500), and the final request path will be host + Users /500 :param dependent: Example {"case_001",["$.data.id","$.data.username"]} Extract the id under data from the actual response result of the use case case_001, and the value of username (assuming id value is 500 and username is admin), {"id":500, "username":"admin"} {"id":500, "username":"admin"}} Response_reg ->response = response_reg->response = response_reg->response = response_reg->response = response_reg->response
# thank: https://www.cnblogs.com/yoyoketang/p/13386145.html, provide dynamic add title example code
# Add title dynamically
allure.dynamic.title(case_title)
logger.debug(F '⬇ ️ ⬇ ️ ⬇ ️... Execute use case number:{case_number}. ⬇ ️ ⬇ ️ ⬇ ️ ️ ')
with allure.step("Handle data dependencies, header") : data, header, parameters_path_url = treat_data.treating_data(is_token, parameters, dependent, data, save_response_dict)with allure.step("Send the request, get the result of the response json string") : res = br.base_requests(method=method, url=base_url + path + parameters_path_url, parametric_key=parametric_key, file_var=file_var, file_path=file_path, data=data, header=header)with allure.step("Write the contents of the response result to the actual response dictionary."):
save_response_dict.save_actual_response(case_key=case_number, case_response=res)
The interface to write tokens must be correct enough to return tokens
if is_token == 'write':
with allure.step("Extract token from logon response into header"):
treat_data.token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0]
with allure.step("Extract the actual data according to the extract response rules of the configuration file"):
really = jsonpath.jsonpath(res, res_reg)[0]
with allure.step("Processing the expected result response read out"):
expect = json.loads(expect)
with allure.step("Predicate the expected result against the actual response.") :assert really == expect
logger.info(F 'Complete JSON response:{res}\n Data dictionary to verify:{really}Expected validation data dictionary:{expect}\n Test results:{really == expect}')
logger.debug(F '⬆ ⬆ ⬆... Use Case Number:{case_number}, log view... ⬆ ⬆ ⬆ \ n \ n ️ ')
if __name__ == '__main__':
TestApiAuto().run_test()
# With Jenkins integration, these two methods will not be used (mail/report zip)
# from tools.zip_file import zipDir
# from tools.send_email import send_email
# zipDir(report_generate, report_zip)
# send_email(email_setting)
Copy the code
Run the test
After ensuring that the required environment and dependencies are all right, open the project using Pycharm and findsettings
To change it to Unitest or other non-PyTest, perform the following steps
The results
Thank you
Jsonpath grammar learning: blog.csdn.net/liuchunming… The zip file compression: www.cnblogs.com/yhleng/p/94… Welcome to exchange.
The source address
Source address: Giee-version1.0 branch: gitee.com/zy7y/apiAut… GitHub- version1.0 branch: github.com/zy7y/apiAut…
update
2020/11/23 – optimization data parameters, path dependence approach, now before the version with the same environment, the test time to 2 s is introduced: www.cnblogs.com/zy7y/p/1402…