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

  1. Implementation: GET/POST request (upload file): theoretically, other DELETE/PUT requests are implemented, and restful interface specifications are supported
  2. Send E-mail
  3. Generate allure test report
  4. Compress the test report file
  5. Data dependencies

Operation mechanism

  1. 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.
  2. Read excel use case file data, form a PyTest parameterized use case data, and process data according to each column (token operation, data dependency)
  3. 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
  4. 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
  5. The actual response results are written back to Excel after each request is completed
  6. Extract the actual response content from the jSONPATH expression configured in the configuration file and compare the data with the expected results in Excel
  7. Generate test report
  8. Compress the test report folder
  9. 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 findsettingsTo 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…