Python API自動テストフレームワークの詳細解説

認証成功時のAPI実装

from common.http_client import *
from common.shared_state import *

class Authentication(HttpClient):

    def authenticate_system(self, username, credential):
        # システム認証処理
        logger.info("システム認証を実行中")
        endpoint = f"https://signinuat.midea.com/login?service=http%3A%2F%2Fdevsecopsuat.midea.com%2Fagile%2Ftest%2Ffunction%2Fcase%3FsysId%3DITS000001096&locale=zh"
        self._execute_get(endpoint=endpoint)
        payload = f'username={username}&password={credential}&execution=e1s1&bVisitorId=5e96ed57febc37d079725bd8f40ea042&_eventId=submit&authType=sms123&geolocation='
        response = self._execute_form_post(endpoint=endpoint, data=payload)
        set_shared_state("SystemSession", self.session)
        return response.text

認証前処理とシステムページ取得API

from common.http_client import *
from common.shared_state import *

class SystemApi(HttpClient):

    def __init__(self):
        self.session = get_shared_state("SystemSession")
        if self.session is None:
            logger.error("システムに認証されていません")
            raise SystemExit

    def fetch_system_data(self, payload):
        endpoint = "https://devsecopsuat.midea.com/api/system-project-server/systemInfo/getListPage"
        response = self._execute_json_post(endpoint=endpoint, data=payload)
        return response.json()

テストケース作成APIの実装

from common.http_client import *
from common.shared_state import *

class TestCaseCreator(HttpClient):
    def __init__(self):
        self.session = get_shared_state("SystemSession")
        if self.session is None:
            logger.error("システムに認証されていません")
            raise SystemExit

    def create_test_case(self, parameters):
        # 新規テストケース作成ボタンクリック
        endpoint = "https://devsecopsuat.midea.com/api/agile-service/workItem/v1/getItemView/VIRTUAL_SYSTEM_TESTCASE_TEMPLATE_ID"
        response = self._execute_get(endpoint=endpoint, params=parameters)
        return response.json()

テストケース削除APIの実装(Gpath式を使用)

from common.http_client import *
from common.shared_state import *

class TestCaseRemover(HttpClient):
    def __init__(self):
        self.session = get_shared_state("SystemSession")
        if self.session is None:
            logger.error("システムに認証されていません")
            raise SystemExit

    def remove_test_case(self, parameters):
        # headersの設定
        headers = {"Teamid": "", "Orgid": "bbe42cba6adb4d268d72ce087123e4c5", "Systemid": "ITS000001096", "View": "system"}
        endpoint = f"https://devsecopsuat.midea.com/api/agile-service/test/delete/<strong>{get_shared_state('case_id')}</strong>/ITS000001096"
        print("endpoint=", endpoint)
        response = self._execute_get(endpoint, data=parameters, headers=headers)
        return response.json()

初期化用の空クラス

動的値取得用ユーティリティクラス(環境変数のカプセル化)

# 共有状態-初期化
def init_shared_state():
    global _shared_dict
    _shared_dict = {}


# 共有状態の設定
def set_shared_state(key, value):
    try:
        # フレームワークが使用する共有変数はログに出力しない
        _shared_dict[key] = value
    except Exception:
        try:
            init_shared_state()
            _shared_dict[key] = value
        except Exception as e:
            raise Exception(f"共有変数の設定中にエラーが発生しました:{key}={value}、例外:{e}")


# 共有変数の取得(存在しない場合はNoneを返す)
def get_shared_state(key):
    try:
        return _shared_dict[key]
    except Exception as e:
        # デフォルトはsit環境
        if key == "environment":
            set_shared_state("environment", "sit")
            return "sit"
        print(f"共有変数の取得中にエラーが発生しました:【{key}】例外:{e}")
        return None

HTTPリクエストをカプセル化したクラス:

import json
import requests
from loguru import logger

class HttpClient:

    session = requests.session()

    def _execute_get(self, endpoint, params=None, data=None, headers=None):
        """
        GETリクエストの実行

        :param endpoint: リクエスト先URL
        :param headers: 任意、リクエストヘッダー
        :param data: 任意、リクエストボディ
        :return: レスポンス内容
        """
        logger.debug(f"リクエストデータ:{data}")
        self.headers = {"Connection": "keep-alive",
                        "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
                        "Accept": "application/json, text/javascript, */*; q=0.01"}
        if headers is not None:
            self.headers.update(headers)
        response = self.session.request(url=endpoint, method="GET", params=params, data=data, headers=self.headers,
                                    allow_redirects=True, timeout=180)
        logger.debug(f"レスポンスデータ:{response.text}")
        return response

    def _execute_form_post(self, endpoint, params=None, data=None, headers=None):
        """
        フォーム形式のPOSTリクエストの実行

        :param endpoint: リクエスト先URL
        :param headers: 任意、リクエストヘッダー
        :param data: 任意、リクエストボディ
        :return: レスポンス内容
        """
        logger.debug(f"リクエストデータ:{data}")
        self.headers = {"Connection": "keep-alive",
                        "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
                        "Accept": "application/json, text/javascript, */*; q=0.01"}
        if headers is not None:
            self.headers.update(headers)
        response = self.session.request(url=endpoint, method="POST", params=params, data=data, headers=self.headers,
                                    allow_redirects=True, timeout=180)
        logger.debug(f"レスポンスデータ:{response.text}")
        return response

    def _execute_json_post(self, endpoint, params=None, data=None, headers=None):
        """
        JSON形式のPOSTリクエストの実行

        :param endpoint: リクエスト先URL
        :param headers: 任意、リクエストヘッダー
        :param data: 任意、リクエストボディ
        :return: レスポンス内容
        """
        if isinstance(data, dict):
            data = json.dumps(data)
        logger.debug(f"リクエストデータ:{data}")
        self.headers = {"Connection": "keep-alive", "Content-Type": "application/json; charset=utf-8",
                        "Accept": "application/json, text/javascript, */*; q=0.01"}
        if headers is not None:
            self.headers.update(headers)
        response = self.session.request(url=endpoint, method="POST", params=params, data=data, headers=self.headers,
                                    allow_redirects=True, timeout=180)
        logger.debug(f"レスポンスデータ:{response.text}")
        return response

リクエストパラメータ読み込み用ユーティリティクラス(YAMLファイル読み込み):

import os, yaml, re

BASE_DIR = os.path.dirname(os.path.realpath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(BASE_DIR, "../"))

# 単一YAMLファイルからデータを読み込む
def load_yaml(yamlPath):
    BASE_DIR = os.path.dirname(os.path.realpath(__file__))
    root_path = os.path.abspath(os.path.join(BASE_DIR, "../"))
    yamlPath = root_path + yamlPath
    with open(yamlPath, encoding='utf-8') as f:
        # YAMLデータの読み込み
        data = yaml.safe_load(f)
        return data

def generate_yaml(data, yamlFile):
    """
    辞書やリストからYAMLファイルを生成する

    :param data: 入力データ(辞書、リスト)
    :param yamlFile: 生成するYAMLファイル名
    """
    try:
        yamlFile = os.path.join(ROOT_PATH + yamlFile)
        with open(yamlFile, "w", encoding="utf-8") as f:
            yaml.dump(data, f, allow_unicode=True)
    except Exception as e:
        raise Exception(f"YAMLファイル生成エラー:{e}")


if __name__ == '__main__':
    pass
    # リクエストデータ:配列、辞書、文字列が可能
    request_data = {"param":{"org_id":"bbe42cba6adb4d268d72ce087123e4c5","sys_name":""},"current":1,"size":10,"startPage":1,"pageSize":10}
    # YAML生成メソッドの呼び出し
    generate_yaml(request_data, "/test_data/sample.yaml")

テストレポート生成用ユーティリティクラス:

# -*- coding: utf-8 -*-
# @File  : shell.py
import subprocess

class CommandExecutor:
    """シェルコマンド実行メソッドをカプセル化"""
    @staticmethod
    def run(command):
        output, errors = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
        o = output.decode("utf-8")
        return o

リクエスト実行用クラス:

import allure

from apis.StoreCase import StoreCase
from apis.ValidateCase import ValidateCase
from apis.DuplicateCase import DuplicateCase
from apis.RemoveCase import RemoveCase
from apis.ExtractCase import ExtractCase
from apis.auth import *
from apis.system import *
from apis.creator import *
from common.yaml_loader import *

class TestSuite:

    #認証
    @allure.feature('認証')
    @allure.story('システムログイン')
    def test_login(self):
        # BlazeMonitorでパケットキャプチャ
        Authentication().authenticate_system("ex_shijl2", "vi3fYRFnStfYLXBriYRLlg==")
        print("認証成功")

        system_api = SystemApi()
        data = load_yaml("/test_data/sample.yaml")
        system_api.fetch_system_data(data)

    #新規テストケース作成ボタンクリック
    @allure.feature('テストケース作成')
    @allure.story('新規作成')
    def test_create_case(self):
        creator = TestCaseCreator()
        data = load_yaml("/test_data/newCase01.yaml")
        creator.create_test_case(data)

    #【保存】ボタンクリック(一意性チェックAPIが呼ばれる)
    @allure.feature('一意性チェック')
    @allure.story('重複確認')
    def test_validate_case(self):
        validator = ValidateCase()
        data = load_yaml("/test_data/checkCase.yaml")
        validator.validate_case(data)

    @allure.feature('テストケース保存')
    @allure.story('保存処理')
    def test_store_case(self):
        storer = StoreCase()
        data = load_yaml("/test_data/saveCase.yaml")
        storer.store_case(data)

    #コピー
    @allure.feature('テストケースコピー')
    @allure.story('複製処理')
    def test_duplicate_case(self):
        duplicator = DuplicateCase()
        data = load_yaml("/test_data/copyCase01.yaml")
        duplicator.duplicate_case(data)

    @allure.feature('コピーしたテストケースの保存')
    @allure.story('複製保存')
    def test_store_duplicate(self):
        storer = StoreCase()
        data = load_yaml("/test_data/saveCopyCase.yaml")
        storer.store_case(data)

    #エクスポート
    @allure.feature('テストケースエクスポート')
    @allure.story('データ出力')
    def test_export_case(self):
        #クラス名の先頭は大文字、YAML内のデータはフォーマットしないこと
        #Authentication().authenticate_system("ex_shijl2", "vi3fYRFnStfYLXBriYRLlg==")
        extractor = ExtractCase()
        extractor.get_system_info({"systemId":"ITS000001096","listView":"drop_down_tree","extMap":{}})
        data = load_yaml("/test_data/exportCase.yaml")
        response = extractor.extract_case(data)
        assert response.status_code == 200
        # with open("./test.xls", 'wb') as f:
        #     f.write(response.content)

    #削除
    @allure.feature('テストケース削除')
    @allure.story('削除処理')
    # allure serve ./result/
    # allure serve ./allure-results
    def test_remove_case(self):
       remover = RemoveCase()
       data = load_yaml("/test_data/deleteCase.yaml")
       remover.remove_case(data)

リクエストパラメータ用YAMLファイルの実際の読み込み設定

起動クラスとレポート読み込みクラス:

import pytest, os, sys
from common.command_executor import CommandExecutor
from loguru import logger

# 実行システム識別子
systemMark = str(sys.platform)

if __name__ == "__main__":
    BASE_DIR = os.path.dirname(os.path.realpath(__file__))
    root_path = os.path.abspath(os.path.join(BASE_DIR))
    # コマンドビルド
    executor = CommandExecutor()
    allure_report = "--alluredir=./allure-results"
    allure_result_path = "./allure-results"
    allure_report_path = "./allure-report"

    try:
        # テストケース実行
        args = ['-v', '-s', 'test_case/', allure_report, '--clean-alluredir', "--reruns=1"]
        pytest.main(args)
        # Allureレポート生成
        cmd = 'allure generate %s -o %s --clean' % (allure_result_path, allure_report_path)
        executor.run(cmd)
        logger.debug("Allureレポートの生成に成功しました")
    except Exception as e:
        logger.error(f'テストケースの実行に失敗しました、{e}')
allure-pytest==2.9.45
allure-python-commons==2.9.45
jmespath==0.9.5
jsonpath==0.82
logger==1.4
numpy==1.24.3
openpyxl==3.0.10
orjson==3.7.7
pandas==2.0.1
pytest==7.1.2
pytest-dependency==0.6.0
pytest-forked==1.4.0
pytest-html==3.1.1
pytest-metadata==2.0.1
pytest-rerunfailures==10.2
pytest-sugar==0.9.4
pytest-xdist==2.5.0
python-dateutil==2.8.2
PyYAML==6.0
regex==2023.8.8
requests==2.27.1
setuptools==56.0.0
simple-settings==1.2.0
six==1.16.0
xlrd==2.0.1

タグ: Python APIテスト 自動化フレームワーク Allure pytest

10月3日 22:32 投稿