pytest+yaml+allure接口自動化測試框架


前言

​ 現在是2022年,自動化測試,是目前測試行業一項比較普遍的測試技術了,之前的以UI自動化測試為主,現在的以接口自動化測試為主,無論技術更迭,自動化測試總有他的重量,用機器代替手工工作,是21世紀不斷進行的課題。

​ 可是身為測試,難受的是腳本容易寫,學幾天python,照貓畫虎三兩天也能寫一個不錯的腳本。可是想更上一層,去搭建一個測試框架卻顯得不是那么容易,曾經我也是這樣的困難。時光不負有心人,學習了漫長時間終於是現在有了一些開發基礎,抽空搞了一個簡單版本的接口自動化測試框架。

希望我的框架能給予你一定啟發的同時,你也能指出一些我的不足之處,互相學習,我們才能共同進步。

環境搭建

目錄文件添加

我們打開vscode新建一個項目,名字就姑且命名為:interface_test_example, 創建好之后,我們就按照這個下面這個目錄結構去創建相應的文件內容。

.
├── common                    	——公共方法目錄
│   ├── cache.py				——緩存文件
│   ├── exceptions.py			——異常處理
│   ├── __init__.py				
│   ├── json.py					——序列化和反序列化
│   ├── regular.py				——正則處理
│   ├── request.py				——請求處理
│   └── result.py				——響應處理
├── conftest.py					——pytest膠水文件
├── environment.properties		——allure配置文件
├── logs						——日志目錄
├── main.py						——主運行文件
├── pytest.ini					——pytest配置文件
├── readme.md					
├── requirements.txt	
├── tests						——測試用例目錄
│   └── testcase.yaml
└── utils						——第三方工具文件
    ├── __init__.py	
    ├── logger.py				——日志
    ├── readme.md
    └── time.py					——時間處理

當你把上面這些內容創建完成之后我們的項目內容就算整體創建完成了。

python虛擬環境創建

在創建之前我先聲明一下我所使用的python版本是3.8.6版本。學習本篇請不要使用3.8版本以下python,某些語法會不支持。

1、創建虛擬環境

python3 -m venv env

2、 安裝requirements.txt的依賴包

pip install -r requirements.txt

requirements.txt的具體內容

allure-pytest==2.9.43
allure-python-commons==2.9.43
pytest==6.2.5
pytest-assume==2.4.3
pytest-html==3.1.1
PyYAML==5.4.1
requests==2.26.0

安裝完成之后我們的環境就搭建好了。

測試用例管理

excel這種總歸是太麻煩了,所以我們需要一個更合適的。挑來選去yaml是最簡單方便的,數據能幾乎無縫切換。

先來看看我們的用例吧,都寫了些什么。打開tests/testcase.yaml文件,輸入以下內容。

config: # 測試信息
  baseurl: "https://www.zhixue.com"
  timeout: 30.0
  headers:
    Accept: application/json, text/javascript, */*; q=0.01
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    Connection: keep-alive
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
    cookies: aliyungf_tc=AQAAANNdlkvZ2QYAIb2Q221oiyiSOfhg; tlsysSessionId=cf0a8657-4a02-4a40-8530-ca54889da838; isJump=true; deviceId=27763EA6-04F9-4269-A2D5-59BA0FB1F154; 6e12c6a9-fda1-41b3-82ec-cc496762788d=webim-visitor-69CJM3RYGHMP79F7XV2M; loginUserName=18291900215
    X-Requested-With: XMLHttpRequest
variable:
  none : none
tests:
  test_login:
    description: "登錄"
    method: post
    route: /weakPwdLogin/?from=web_login
    RequestData:
      data:
        loginName: 18291900215
        password: dd636482aca022
        code:
        description: encrypt
    Validate:
      expectcode: 200
      resultcheck: '"result":"success"'
      regularcheck: '[\d]{16}'
    Extract:
      - data
  test_login_verify:
    description: "驗證登錄"
    method: post
    route: /loginSuccess/
    RequestData:
      data:
        userId: "${data}"
    Validate:
      expectcode: 200
      regularcheck:
      resultcheck: '"result":"success"'

第一部分config內容:主要是一些全局的配置信息,如請求地址、請求頭等。

第二部分variable內容:主要是預先設置一些全局變量等等內容。比如可以加入郵箱地址等等。

第三部分tests內容:這個是真正的測試用例部分,通過匹配requests庫的輸入參數,以簡潔明了的寫法更好的支持測試。

日志封裝

打開utils/logger.py文件,這個utils的意思是一個工具包的意思。在這個里面我們主要存放可以獨立運行的工具模塊。比如日志文件就是一個可以獨立運行的。打開之后我們輸入以下的內容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
日志類
"""
import os
import logging
from logging.handlers import RotatingFileHandler


def init_logger():
    """初始化日志"""
    basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    debug_file = os.path.join(basedir, 'logs', 'server.log')

    logger_formatter = logging.Formatter(
        '%(levelname)s %(asctime)s [%(filename)s:%(lineno)s] %(thread)d %(message)s')

    # debug
    logger_debug = logging.getLogger('apitest')
    handler_debug = RotatingFileHandler(debug_file,
                                        encoding='utf-8',
                                        maxBytes=20 * 1024 * 1024,
                                        backupCount=10)
    handler_debug.setFormatter(logger_formatter)
    logger_debug.setLevel(logging.DEBUG)
    logger_debug.addHandler(handler_debug)
    # 在控制台輸出
    return logger_debug


logger = init_logger()

if __name__ == '__main__':
    logger.debug("debug")
    logger.info("info")
    logger.warning('warning')
    logger.error("error")
    logger.critical('critical')

下面一些日志輸入示例。我們來執行一下。

image-20210924220036286

可以看到成功的在日志文件中寫入了新的信息。

緩存工具

是的你沒看錯,我給它起的名字就叫緩存,其實內部組成本質就是一個python字典。而不是你想的redis這種。

打開common/cache.py文件,我們輸入以下內容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
緩存類
"""
from collections import UserDict


class CachePool(UserDict):
    """全局變量池"""

    def get(self, key, default=None):
        return self.data.get(key, default)

    def set(self, key, value = None):
        self.data.setdefault(key, value)

    def has(self, key):
        return key in self.data

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return bool(self.data)


cache = CachePool()

if __name__ == '__main__':
    cache.set('name', 'wxhou')
    print(len(cache))
    print(cache.get('name'))

我們執行測試一下:

image-20210924221323950

可以看到沒有問題。通過這個字典我們把一些臨時的信息放在這個里面,因為只是示例項目,用redis顯得有些麻煩,采用這種方式更為簡便一些。

讀取yaml測試用例

​ 使用yaml作為測試用例,我們就需要對文件的內容進行讀取,常規來說的應該是通過pyyaml對讀取到的內容進行數據解析,然后使用pytest parametrize參數化功能進行數據參數化用例測試。但是完事之后,這樣的方式好像不是很優雅,寫的代碼組織起來比較費勁,於是乎,我在pytest的官方文檔中,發現了一套更為一套非常優雅的測試執行方式,他們稱之為non-python test的測試模式。

具體內容可以查看官方文檔,感興趣的可以去看看:Working with non-python tests — pytest documentation

# content of conftest.py
import pytest


def pytest_collect_file(parent, path):
    if path.ext == ".yaml" and path.basename.startswith("test"):
        return YamlFile.from_parent(parent, fspath=path)


class YamlFile(pytest.File):
    def collect(self):
        # We need a yaml parser, e.g. PyYAML.
        import yaml

        raw = yaml.safe_load(self.fspath.open())
        for name, spec in sorted(raw.items()):
            yield YamlItem.from_parent(self, name=name, spec=spec)


class YamlItem(pytest.Item):
    def __init__(self, name, parent, spec):
        super().__init__(name, parent)
        self.spec = spec

    def runtest(self):
        for name, value in sorted(self.spec.items()):
            # Some custom test execution (dumb example follows).
            if name != value:
                raise YamlException(self, name, value)

    def repr_failure(self, excinfo):
        """Called when self.runtest() raises an exception."""
        if isinstance(excinfo.value, YamlException):
            return "\n".join(
                [
                    "usecase execution failed",
                    "   spec failed: {1!r}: {2!r}".format(*excinfo.value.args),
                    "   no further details known at this point.",
                ]
            )

    def reportinfo(self):
        return self.fspath, 0, f"usecase: {self.name}"


class YamlException(Exception):
    """Custom exception for error reporting."""

可以看到官方文檔中以極其優雅的方式通過yaml文件驅動了兩個測試用例。我們也將在此基礎上進行擴展衍生。

我們根據官方文檔中的示例文件,在這個基礎上進行修改,加入我們的內容。

pytest_collect_file

首先我們修改pytest_collect_file函數中的內容,讓他支持yamlyml兩種格式的文件內容。因為這兩種都可以,官網示例中只有一個。

if path.ext in (".yaml", ".yml") and path.basename.startswith("test"):
    return YamlFile.from_parent(parent, fspath=path)

YamlFile.collect

接下來修改我們的YamlFile.collect方法,這里面就是對讀出來的詳細內容按照設置的格式進行處理,該存入緩存的放入緩存,該執行測試的時候執行測試。

if not any(k.startswith('test') for k in raw.keys()):
    raise YamlException("{}yaml non test found".format(self.fspath))

通過這個語句我們先判斷一下,有沒有測試用例,如果沒有測試用例我們直接就報錯了,不在執行,拋出異常,這個異常需要我們自己封裝一下。我們打開common/exceptions.py文件。輸入以下內容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
異常類
"""
from requests.exceptions import RequestException


class YamlException(Exception):
    """Custom exception for error reporting."""

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return "\n".join(
            [
                "usecase execution failed",
                "   spec failed: {}".format(self.value),
                "   For more details, see this the document.",
            ]
        )

這個就是當我們發現yaml文件中沒有符合的測試標簽內容后拋出的異常類。

然后我們接着先讀取全局變量:

if variable := raw.get('variable'):
    for k, v in variable.items():
        cache.set(k, v)

我們把yaml文件中預設的全局變量信息中全部存在我們設置的緩存模塊中,這樣在測試過程中我們可以隨時的去用。

繼續讀取配置文件。

if config := raw.get('config'):   
    for k, v in config.items():
        cache.set(k, v)

然后我們讀取常用的測試信息也放入緩存之中,方便運行過程中隨時去調用。

最后我們來處理一下。測試用例部分:

if tests := raw.get('tests'):
    for name, spec in tests.items():
        yield YamlTest.from_parent(self,
                                   name=spec.get('description') or name,
                                   spec=spec)

可以看到,在官方文檔中使用了sorted函數進行了再次排序。我這里沒有是因為再次排序會破壞用例的結構和順序。最后輸出的時候spec.get('description') or name的寫法先獲取yaml文件中我們設置的中文標識,如果中文標識不存在則繼續使用英文標識。其余和官方文檔保持一致。

以上就是做出的改動,我們來看看吧:

import yaml
import pytest
from common.cache import cache
from common.exceptions import YamlException


def pytest_collect_file(parent, path):
    if path.ext in (".yaml", ".yml") and path.basename.startswith("test"):
        return YamlFile.from_parent(parent, fspath=path)


class YamlFile(pytest.File):

    def collect(self):
        raw = yaml.safe_load(self.fspath.open(encoding='utf-8'))
        if not any(k.startswith('test') for k in raw.keys()):
            raise YamlException("{}yaml non test found".format(self.fspath))
        if variable := raw.get('variable'):
            for k, v in variable.items():
                cache.set(k, v)
        if config := raw.get('config'):
            for k, v in config.items():
                cache.set(k, v)
        if tests := raw.get('tests'):
            for name, spec in tests.items():
                yield YamlTest.from_parent(self,
                                           name=spec.get(
                                               'description') or name,
                                           spec=spec)

站在巨人的肩膀上才能看得更遠。在pytest non-python tests的內容之上做了一些改動,使得讀取文件更加貼合我們定義的yaml文件內容。在精簡了很多代碼的同時我們也達到了預期的效果。

處理request

​ 談到HTTP請求,我們首先就會想到requests庫,這個第三方庫,以極其優雅的封裝方式和簡易的寫法,在python界有着重要的地位,在這個接口自動化測試框架中,我們也會使用這個庫進行二次封裝。讓其融入到我們的測試框架中來。

​ 對於這個庫我就不做過多的介紹了,之前有一篇文章專門介紹,感興趣可以點擊:python requests由淺入深 - 隨風揮手 - 博客園 (cnblogs.com)

執行測試的代碼

上一章節已經講了怎么讀取測試用例數據,根據pytest官網的non-python test內容,我們還需要編寫一個YamlTest類來執行測試。

繼續打開conftest.py文件,在里面加上如下內容:

# +++
from common.request import HttpRequest
from common.exceptions import RequestException

# +++

class YamlTest(pytest.Item):
    def __init__(self, name, parent, spec):
        super(YamlTest, self).__init__(name, parent)
        self.spec = spec
        self.request = HttpRequest(exception=(RequestException, Exception))

    def runtest(self):
        """Some custom test execution (dumb example follows)."""
        self.request.send_request(**self.spec)

    def repr_failure(self, excinfo):
        """Called when self.runtest() raises an exception."""
        logger.critical(excinfo.value)
        logger.critical(excinfo.traceback[-6:-1])            

    def reportinfo(self):
        return self.fspath, 0, f"usecase: {self.name}"

通過繼承pytest.Item類我們可以使用父類的運行測試的方法來執行測試。

__init__方法

在這個里面我們接收來自yamlfile類中collect方法的yield生成器傳給我們的測試數據。

runtest

繼承父類的runtest方法我們可以在這個里面執行我們的測試,把接受到的參數傳入我們二次封裝的HttpRequest類,就可以對我們在yaml文件中添加的接口進行測試了。

repr_failure

如果在運行中發生了用例失敗的現象我們可以在這個方法中攔截並打印出相應的報錯信息,方便我們排查問題。

reportinfo

通過reportinfo方法重寫我們傳入的name信息,就是我們在yaml文件中的測試用例名稱信息。

這個就是我們通過對YamlTest的改造,組成了一個測試過程。這個類的核心是對requests的二次封裝類。

二次封裝requests

我們打開common/request.py,我們鍵入以下內容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
requests二次封裝
"""
import urllib3
from requests import Session, Response
from common.cache import cache
from utils.logger import logger

urllib3.disable_warnings()


class HttpRequest(Session):
    """requests方法二次封裝"""

    def __init__(self, *args: t.Union[t.Set, t.List], **kwargs: t.Dict[t.Text, t.Any]):
        super(HttpRequest, self).__init__()
        self.exception = kwargs.get("exception", Exception)

    def send_request(self, **kwargs: t.Dict[t.Text, t.Any]) -> Response:
        """發送請求
        """
        try:
            logger.info("request data: {}".format(kwargs))
            method = kwargs.get('method', 'GET').upper()
            url = cache.get('baseurl') + kwargs.get('route')
            logger.info("Request Url: {}".format(url))
            logger.info("Request Method: {}".format(method))
            logger.info("Request Data: {}".format(kwargs))
            request_data = HttpRequest.mergedict(kwargs.get('RequestData'),
                                                 headers=cache.get('headers'),
                                                 timeout=cache.get('timeout'))
            response = self.dispatch(method, url, **request_data)
            logger.info("Request Result: {}{}".format(response, response.text))
            return response
        except self.exception as e:
            logger.exception(format(e))
            raise e

    def dispatch(self, method, *args, **kwargs):
        """請求分發"""
        handler = getattr(self, method.lower())
        return handler(*args, **kwargs)

    @staticmethod
    def mergedict(args, **kwargs):
        """合並字典"""
        for k, v in args.items():
            if k in kwargs:
                kwargs[k] = {**args[k], **kwargs.pop(k)}
        args.update(kwargs)
        return args

我們通過繼承requests庫的Session類,添加我們的定制化的一些方法。

send_request方法

我們把YamlTest類中的測試用例數據傳入到我們的這個方法中來,然后打印日志記錄,並將結果進行返回。

dispatch

在這個方法中我們根據傳入的用例請求方法,去反射我們Session類中的相應的請求方法,從而實現get,post等HTTP請求。

mergedict

編寫了一個合並字典的方法,用來合並我們定義的請求體或者請求參數,和我們自定義的一些測試配置,比如headers,timeout等。

對於requests的封裝暫時就介紹到這里。

處理response

我們已經基本完成了測試框架的前半部分工作,剩下的章節基本都是后半部分內容了。這個章節我們來說一下我們獲取到請求的結果之后怎么處理response(響應)。本來計划序列化和反序列化放一個章節的,但是鑒於內容有點少,所以我把4 5章節合並了。

序列化和反序列化

我們打開common/json.py文件。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
序列化和反序列化類
"""
import json


def loads(content):
    """
    反序列化
        json對象 -> python數據類型
    """
    return json.loads(content)


def dumps(content, ensure_ascii=True):
    """
    序列化
        python數據類型 -> json對象
    """
    return json.dumps(content, ensure_ascii=ensure_ascii)


def is_json_str(string):
    """驗證是否為json字符串"""
    try:
        json.loads(string)
        return True
    except:
        return False

我們通過自帶的json模塊,封裝兩個方法

  • loads,這個主要用來把json字符串轉換為python對象。
  • dumps,主要用來把python對象轉換成json格式。
  • is_json_str我們可能需要對一個字符串是不是json格式需要做驗證,所以我們寫一個這樣的方法。

正則處理

在開始對response進行處理之前,我們需要封裝一下正則方法。

打開common/regular文件,輸入以下內容。

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
正則相關操作類
"""
import re
from common.json import is_json_str
from utils.logger import logger


def get_var(key, raw_str):
    """獲取變量"""
    if is_json_str(raw_str):
        return re.compile(r'\"%s":"(.*?)"' % key).findall(raw_str)[0]
    return re.compile(r'%s' % key).findall(raw_str)[0]

這個的目的就是為了我們能在json數據中,通過名稱能夠獲取到名稱所對應的值。例如有以下字符串A

{"username":"admin"}

我們能夠通過get_var(username, A),獲取到admin的信息。

處理result

當我們把准備工作做好之后我們就可以在result.py。文件中對我們的內容進行處理了。

我們打開common/result.py,輸入以下內容:

# -*- coding: utf-8 -*-
__author__ = 'wxhou'
__email__ = '1084502012@qq.com'
"""
response響應處理
"""
import re
import pytest
from common.cache import cache
from common.regular import re, get_var
from utils.logger import logger


def check_results(r, validate):
    """檢查運行結果"""
    expectcode = validate.get('expectcode')
    resultcheck = validate.get('resultcheck')
    regularcheck = validate.get('regularcheck')
    if expectcode:
        pytest.assume(expectcode == r.status_code)
    if resultcheck:
        pytest.assume(resultcheck in r.text)
    if regularcheck:
        pytest.assume(re.findall(regularcheck, r.text))

可以看到我封裝了檢查運行結果的函數,這個里里面我用了一個類庫。pytest-assume用過的朋友應該知道這個有什么作用。

官方地址:https://github.com/astraw38/pytest-assume

該插件的主要作用是,在斷言失敗后繼續運行,並且會統計斷言的報錯情況。能夠保證完整的運行,不會因為一個錯誤而發生整個測試停止的問題。

這個添加好之后我們,接着打開conftest.py文件,在YamlTest類中把我們這個方法集成進去。

from common.result import check_results

	+++
    
    def runtest(self):
        """Some custom test execution (dumb example follows)."""
        r = self.request.send_request(**self.spec)
        self.response_handle(r, self.spec.get('Validate'))

    def response_handle(self, r, validate):
        """Handling of responses"""
        if validate:
            check_results(r, validate)
            
    +++

我們在文件中添加以上內容。我們先創建一個response_handle處理方法。然后在runtest執行的時候導入這個方法,通過傳入,請求的返回和需要驗證的結果,通過check_result方法,我們基本就達到了簡單的返回驗證。

當然了我們這個只是最簡單的,可能還有一些更復雜的,比如對數據的格式驗證,和數據的返回層級驗證,與數據庫中的數據進行對比等驗證操作。但是我這個只是一個簡單的測試框架,還沒有那么重,只是提供一種思路,剩下的實現就要靠你自己了,加油。

接口上下文關聯

前面我們已經完成了測試框架的主要功能了,讀取用例,執行用例,獲取結果。在這個請求中間呢,我們沒有解決一個接口測試中很常見的問題,接口上下文參數傳遞,這個是什么意思呢。

比如我們可以用登錄和登錄驗證這兩個接口來講一下,現在常用的系統都是前后端分離的,認證也是通過JWT的方式來搞定的,那么在登錄接口進行登錄之后就會生成一個token,我們拿到這個token就可以去其他接口進行鑒權,然后才能得到登錄驗證接口返回值。

所以我們這一章就解決一下這個請求參數上下文傳遞。

獲取token

先梳理一下思路,我們第一個請求的接口是登錄接口,它會給我們返回token值,然后傳到下一個接口中。所以我們按照執行順序,先解決拿到返回值這一步。

在yaml文件中我們定義了一個字段Extract,這個字段就是預設一下我們要拿到哪一個值,你得告訴你的程序要那個他才能執行,在這個項目中我們想拿到的就是data這個。

  test_login:
    description: "登錄"
    method: post
    route: /weakPwdLogin/?from=web_login
    RequestData:
      data:
        loginName: 18291900215
        password: dd636482aca022
        code:
        description: encrypt
    Validate:
      expectcode: 200
      resultcheck: '"result":"success"'
      regularcheck: '[\d]{16}'
    Extract:   ---> 注意這一行
      - data

然后我們繼續打開common/result.py這個文件,創建一個函數get_result,獲取一下請求值。

def get_result(r, extract):
    """獲取值"""
    for key in extract:
        value = get_var(key, r.text)
        logger.debug("正則提取結果值:{}={}".format(key, value))
        cache.set(key, value)
        pytest.assume(key in cache)

這個函數的主要工作就是,通過正則表達式獲取到結果,然后把他放入到緩存中去。

更新response_handle

創建好之后,我們就需要去我們處理請求得地方把這個函數,給他嵌套進去。

打開conftest.py文件。

from common.result import get_result, check_results
    
    +++
    
    
    def response_handle(self, r: Response, validate: t.Dict, extract: t.List):
        """Handling of responses"""
        if validate:
            check_results(r, validate)
        if extract:
            get_result(r, extract)

好了到這一步,我們的獲取token(data)的工作就完成了。

接下來我們要處理的是傳入到下一個接口中。

打開YAML測試文件,我們找到測試驗證這條用例。我們會發現有一個${data},這是我們定義的一種變量格式。通過識別變量名稱,去替換相應的結果。

  test_login_verify:
    description: "驗證登錄"
    method: post
    route: /loginSuccess/
    RequestData:
      data:
        userId: "${data}"   ---> 這行
    Validate:
      expectcode: 200
      regularcheck:
      resultcheck: '"result":"success"'

進行替換

我們首先得封裝兩個方法,一個方法讓我們可以獲取到這個用例里面有哪些我們需要替換的變量,一個方法可以讓我們執行這個替換的過程。

打開common/regular.py.

from string import Template
from common.cache import cache

+++

def findalls(string):
    """查找所有"""
    key = re.compile(r"\${(.*?)\}").findall(string)
    res = {k: cache.get(k) for k in key}
    logger.debug("需要替換的變量:{}".format(res))
    return res


def sub_var(keys, string):
    """替換變量"""
    s = Template(string)
    res = s.safe_substitute(keys)
    logger.debug("替換結果:{}".format(res))
    return res

  • findalls

    我們通過正則去查找這個用例下有那些變量需要我們去替換。同時把需要替換的變量和變量值,以字典的形式進行存儲。

  • sub_var

    通過python官方的string模塊中的Template方法,我們可以輕松完成替換,因為我們的變量格式和該模塊中的保持了一致。

編寫好之后,我們打開common/request.py模塊。

from common.json import json, loads, dumps

+++
    
class HttpRequest(Session):
    """requests方法二次封裝"""

    def __init__(self, *args, **kwargs):
        super(HttpRequest, self).__init__()
        self.exception = kwargs.get("exception", Exception)

    def send_request(self, **kwargs):
        try:
            +++
    		logger.info("Request Url: {}".format(url))
            logger.info("Request Method: {}".format(method))
            kwargs_str = dumps(kwargs)
            if is_sub := findalls(kwargs_str):
                kwargs = loads(sub_var(is_sub, kwargs_str))
            logger.info("Request Data: {}".format(kwargs))
            request_data = HttpRequest.mergedict(kwargs.get('RequestData'),
                                                 headers=cache.get('headers'),
                                                 timeout=cache.get('timeout'))
            +++
    +++

我們對send_request方法進行改造,在這里我們就用到了我們上一章編寫的序列化和反序列化方法。

我們先把請求的dict數據,通過反序列化轉換為json字符串。傳給findalls方法獲取到我們需要替換的變量。然后在調用我們編寫的sub_var進行字符串的模板替換,生成新的json字符串,然后在通過序列化方法轉換為dict數據,傳給requests進行請求,這樣我們就實現了,接口的上下文參數傳遞。是不是非常簡單呢。

在完成以上操作后我們可以執行一下看看。

(env) > pytest
================================================================= test session starts =================================================================
platform win32 -- Python 3.8.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0   
rootdir: D:\VScode\Interface_test_example, configfile: pytest.ini       
plugins: assume-2.4.3, html-3.1.1, metadata-1.11.0
collecting ... 
----------------------------------------------------------------- live log collection ----------------------------------------------------------------- 
DEBUG 22:33:59 [regular.py:19] 11052 需要替換的變量:{}
DEBUG 22:33:59 [regular.py:27] 11052 替換結果:{"baseurl": "https://www.zhixue.com", "timeout": 30.0, "headers": {"Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", "cookies": "aliyungf_tc=AQAAANNdlkvZ2QYAIb2Q221oiyiSOfhg; tlsysSessionId=cf0a8657-4a02-4a40-8530-ca54889da838; isJump=true; deviceId=27763EA6-04F9-4269-A2D5-59BA0FB1F154; 6e12c6a9-fda1-41b3-82ec-cc496762788d=webim-visitor-69CJM3RYGHMP79F7XV2M; loginUserName=18291900215", "X-Requested-With": "XMLHttpRequest"}}
collected 2 items                                                                                                                                       

tests/testcase.yaml::\u767b\u5f55
-------------------------------------------------------------------- live log call -------------------------------------------------------------------- 
INFO 22:33:59 [request.py:51] 11052 request data: {'description': '登錄', 'method': 'post', 'route': '/weakPwdLogin/?from=web_login', 'RequestData': {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}, 'Validate': {'expectcode': 200, 'resultcheck': 
'"result":"success"', 'regularcheck': '[\\d]{16}'}, 'Extract': ['data']}
INFO 22:33:59 [request.py:54] 11052 Request Url: https://www.zhixue.com/weakPwdLogin/?from=web_login
INFO 22:33:59 [request.py:55] 11052 Request Method: POST
DEBUG 22:33:59 [regular.py:19] 11052 需要替換的變量:{}
INFO 22:33:59 [request.py:59] 11052 Request Data: {'description': '登錄', 'method': 'post', 'route': '/weakPwdLogin/?from=web_login', 'RequestData': {'data': {'loginName': 18291900215, 'password': 'dd636482aca022', 'code': None, 'description': 'encrypt'}}, 'Validate': {'expectcode': 200, 'resultcheck': 
'"result":"success"', 'regularcheck': '[\\d]{16}'}, 'Extract': ['data']}
INFO 22:34:00 [request.py:73] 11052 Request Result: <Response [200]>{"data":"1500000100070008427","result":"success"}
DEBUG 22:34:01 [result.py:21] 11052 正則提取結果值:data=1500000100070008427                                                                                                      
INFO 22:34:01 [request.py:51] 11052 request data: {'description': '驗證登錄', 'method': 'post', 'route': '/loginSuccess/', 'RequestData': {'data': {'userId': '${data}'}}, 'Validate': {'expectcode': 200, 'regularcheck': None, 'resultcheck': '"result":"success"'}}
INFO 22:34:01 [request.py:54] 11052 Request Url: https://www.zhixue.com/loginSuccess/
INFO 22:34:01 [request.py:55] 11052 Request Method: POST
DEBUG 22:34:01 [regular.py:19] 11052 需要替換的變量:{'data': '1500000100070008427'}
DEBUG 22:34:01 [regular.py:27] 11052 替換結果:{"description": "\u9a8c\u8bc1\u767b\u5f55", "method": "post", "route": "/loginSuccess/", "RequestData": {"data": {"userId": "1500000100070008427"}}, "Validate": {"expectcode": 200, "regularcheck": null, "resultcheck": "\"result\":\"success\""}}
INFO 22:34:01 [request.py:59] 11052 Request Data: {'description': '驗證登錄', 'method': 'post', 'route': '/loginSuccess/', 'RequestData': {'data': {'userId': '1500000100070008427'}}, 'Validate': {'expectcode': 200, 'regularcheck': None, 'resultcheck': '"result":"success"'}}
INFO 22:34:01 [request.py:73] 11052 Request Result: <Response [200]>{"result":"success"}
PASSED                                                                                                                                           [100%] 

可以看到執行成功了,經歷了這么多我們才算創建了一個簡單的接口自動化測試框架。

allure配置

allure的安裝這里就不在說了,之前有一篇文章是已經說過了,
pytest使用allure

配置allure信息

安裝好之后,我們先打開common/request.py文件,在里面做一下修改。

import allure

+++

    def send_request(self, **kwargs: t.Dict[t.Text, t.Any]) -> Response:
            response = self.dispatch(method, url, **request_data)
            description_html = f"""
            <font color=red>請求方法:</font>{method}<br/>
            <font color=red>請求地址:</font>{url}<br/>
            <font color=red>請求頭:</font>{str(response.headers)}<br/>
            <font color=red>請求參數:</font>{json.dumps(kwargs, ensure_ascii=False)}<br/>
            <font color=red>響應狀態碼:</font>{str(response.status_code)}<br/>
            <font color=red>響應時間:</font>{str(response.elapsed.total_seconds())}<br/>
            """
            allure.dynamic.description_html(description_html)
            logger.info("Request Result: {}{}".format(response, response.text))
            return response

在執行請求的時候我們記錄一下,該次請求的詳情信息。
接着我們打開,common/result.py,更新一下處理結果文件的代碼。

import allure

+++


def get_result(r, extract):
    """獲取值"""
    for key in extract:
        value = get_var(key, r.text)
        logger.debug("正則提取結果值:{}={}".format(key, value))
        cache.set(key, value)
        pytest.assume(key in cache)
    with allure.step("提取返回結果中的值"):
        for key in extract:
            allure.attach(name="提取%s" % key, body=cache.get(key))


def check_results(r, validate):
    """檢查運行結果"""
    expectcode = validate.get('expectcode')
    resultcheck = validate.get('resultcheck')
    regularcheck = validate.get('regularcheck')
    if expectcode:
        with allure.step("校驗返回響應碼"):
            allure.attach(name='預期響應碼', body=str(expectcode))
            allure.attach(name='實際響應碼', body=str(r.status_code))
        pytest.assume(expectcode == r.status_code)
    if resultcheck:
        with allure.step("校驗響應預期值"):
            allure.attach(name='預期值', body=str(resultcheck))
            allure.attach(name='實際值', body=r.text)
        pytest.assume(resultcheck in r.text)
    if regularcheck:
        with allure.step("正則校驗返回結果"):
            allure.attach(name='預期正則', body=regularcheck)
            allure.attach(name='響應值', body=str(
                re.findall(regularcheck, r.text)))
        pytest.assume(re.findall(regularcheck, r.text))

把上面這些工作加好之后,我們在命令行運行一下,帶allure報告的cmd

pytest --html=report.html --self-contained-html --alluredir allure-results --clean-alluredir
allure generate allure-results -c -o allure-report
allure open allure-report

查看運行結果:
image

image

可以看到我們成功的把allure報告集成進來了,是不是很簡單又很方便。

Jenkins集成

安裝Jenkins我就不在詳細介紹了。

我們直接創建一個任務。

image

配置git倉庫地址

image

增加構建時的運行命令

image

構建后的操作

image

我們執行一下:

image

發送郵件

發送郵件之前已經說過了,將不再贅述。

原文鏈接:https://www.cnblogs.com/wxhou/p/13184939.html

開源地址

由於智學網進行了接口調整,所以目前測試用例只有一個了。

到了這里我們的簡易版的接口自動化測試框架已經全部完成了。為了方便學習交流,我們此項目的源代碼放在了碼雲

https://gitee.com/wxhou/interface_test_example

歡迎各位在此交流學習,並留下你的想法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM