Python APIテストフレームワークの高度化:テストケースの基礎クラス、タグ機能、失敗ケースの再実装

テストケースの基礎クラスの活用

各テストケースではExcelからデータを読み込み、データを解析、リクエストを送信、レスポンス結果を検証する必要があります。これらの共通処理をBaseCaseという基礎クラスに封装し、テストケースの記述を簡略化できます。

testディレクトリを再編成し、ケースファイルをtest/caseフォルダに格納し、カスタムTestSuiteをtest/suiteフォルダに格納します。 test_user_data.xlsxdata_type列を追加しました。FORMはフォーム形式リクエストを、JSONはJSON形式リクエストを示します。

プロジェクトのtest/caseフォルダにbasecase.pyを作成します:

import unittest
import requests
import json
import sys
sys.path.append("../..")   # パッケージの検索パスをプロジェクトルートに設定

from lib.read_excel import *  
from lib.case_log import log_case_info 

class BaseCase(unittest.TestCase):   # unittest.TestCaseを継承
    @classmethod
    def setUpClass(cls):
        if cls.__name__ != 'BaseCase':
            cls.data_list = excel_to_list(data_file, cls.__name__)

    def get_case_data(self, case_name):
        return get_test_data(self.data_list, case_name)

    def send_request(self, case_data):
        case_name = case_data.get('case_name')
        url = case_data.get('url')
        args = case_data.get('args')
        headers = case_data.get('headers')
        expect_res = case_data.get('expect_res')
        method = case_data.get('method')
        data_type = case_data.get('data_type')

        if method.upper() == 'GET':   # GETリクエスト
            res = requests.get(url=url, params=json.loads(args))

        elif data_type.upper() == 'FORM':   # フォーム形式リクエスト
            res = requests.post(url=url, data=json.loads(args), headers=json.loads(headers))
            log_case_info(case_name, url, args, expect_res, res.text)
            self.assertEqual(res.text, expect_res)
        else:
            res = requests.post(url=url, json=json.loads(args), headers=json.loads(headers))   # JSON形式リクエスト
            log_case_info(case_name, url, args, json.dumps(json.loads(expect_res), sort_keys=True),
                          json.dumps(res.json(), ensure_ascii=False, sort_keys=True))
            self.assertDictEqual(res.json(), json.loads(expect_res))

簡略化されたテストケース: test/case/user/test_user_login.py

from test.case.basecase import BaseCase


class TestUserLogin(BaseCase):   # BaseCaseを直接継承
    def test_user_login_normal(self):
        """レベル1:正常なログイン"""
        case_data = self.get_case_data("test_user_login_normal")
        self.send_request(case_data)

    def test_user_login_password_wrong(self):
        """パスワード誤りログイン"""
        case_data = self.get_case_data("test_user_login_password_wrong")
        self.send_request(case_data)

test/case/user/test_user_reg.py

from test.case.basecase import BaseCase
from lib.db import *
import json


class TestUserReg(BaseCase):

    def test_user_reg_normal(self):
        case_data = self.get_case_data("test_user_reg_normal")

        # 環境チェック
        username = json.loads(case_data.get("args")).get('name')  # 范冰冰
        if check_user(username):
            del_user(username)
        # リクエスト送信
        self.send_request(case_data)
        # データベース検証
        self.assertTrue(check_user(username))
        # 環境クリーンアップ
        del_user(username)

    def test_user_reg_exist(self):
        case_data = self.get_case_data("test_user_reg_exist")

        username = json.loads(case_data.get("args")).get('name')
        # 環境チェック
        if not check_user(username):
            add_user(username, '123456')

        # リクエスト送信
        self.send_request(case_data)

より柔軟な実行方法

これまでのrun_all.pyではすべてのテストケースを実行するしかありませんでした。いくつかの機能を追加して、より柔軟な実行戦略を提供します。

カスタムTestSuiteの実行

プロジェクトのtest/suiteフォルダにtest_suites.pyを作成します:

import unittest
import sys
sys.path.append("../..")
from test.case.user.test_user_login import TestUserLogin
from test.case.user.test_user_reg import TestUserReg

smoke_suite = unittest.TestSuite()  # カスタムTestSuite
smoke_suite.addTests([TestUserLogin('test_user_login_normal'), TestUserReg('test_user_reg_normal')])

def get_suite(suite_name):    # TestSuite取得メソッド
    return globals().get(suite_name)

run_all.pyrun.pyに変更し、run_suite()メソッドを追加します:

import unittest
from lib.HTMLTestReportCN import HTMLTestRunner
from config.config import *
from lib.send_email import send_email
from test.suite.test_suites import *

def discover():
    return unittest.defaultTestLoader.discover(test_case_path)

def run(suite):
    logging.info("================================== テスト開始 ==================================")
    with open(report_file, 'wb') as f: 
         HTMLTestRunner(stream=f, title="APIテスト", description="テスト説明", tester="テスター").run(suite)
   
    # send_email(report_file)  
    logging.info("================================== テスト終了 ==================================")

def run_all():  # すべてのテストケースを実行
    run(discover())

def run_suite(suite_name):  # `test/suite/test_suites.py`で定義されたカスタムTestSuiteを実行
    suite = get_suite(suite_name)
    if suite:
        run(suite)
    else:
        print("TestSuiteが存在しません")

すべてのテストケースを一覧表示(実行はしない)

run.pyに以下を追加します:

def collect():   # discover()で作成されたTestSuiteはフォルダ構造に応じて多階層になっているため、すべてのテストケースを取り出して階層なしのTestSuiteに格納します
    suite = unittest.TestSuite()

    def _collect(tests):   # 再帰処理、下位要素がTestSuiteの場合はさらに下へ
        if isinstance(tests, unittest.TestSuite):
            if tests.countTestCases() != 0:
                for i in tests:
                    _collect(i)
        else:
            suite.addTest(tests)  # 下位要素がTestCaseの場合はTestSuiteに追加

    _collect(discover())
    return suite

def collect_only():   # すべてのテストケースを一覧表示のみ
    t0 = time.time()
    i = 0
    for case in collect():
        i += 1
        print("{}.{}".format(str(i), case.id()))
    print("----------------------------------------------------------------------")
    print("{}件のテストケースを{:.3f}秒で収集".format(str(i),time.time()-t0))

testlistに基づいてテストケースを実行

testフォルダにtestlist.txtを作成し、以下のように記述します:

test_user_login_normal
test_user_reg_normal
# test_user_reg_exist   # コメントアウトすると実行しない

run.pyに以下を追加します:

def makesuite_by_testlist(testlist_file):  # test_list_fileはconfig/config.pyで設定
    with open(testlist_file) as f:
        testlist = f.readlines()

    testlist = [i.strip() for i in testlist if not i.startswith("#")]   # 各行末尾の"/n"と#で始まる行を削除

    suite = unittest.TestSuite() 
    all_cases = collect()  # すべてのテストケース
    for case in all_cases:  # すべてのテストケースからテストメソッド名をマッチング
        if case._testMethodName in testlist:
            suite.addTest(case)
    return suite

テストケースのタグに基づいて実行

TestSuiteは事前に作成する必要がありますが、各テストケースにタグを追加して指定したタグのテストケースを実行することで、より柔軟になります。 残念なことにunittestにはタグ関連機能がないため、以下のような実装が考えられます:

def tag(tag):
    if tag==OptionParser.options.tag:   # 実行時のコマンドライン引数
        return lambda func: func    # テストケースのタグがコマンドライン指定のタグと一致する場合、テストケース自体を返す
    return unittest.skip("このタグを含まないテストケースをスキップ")    # そうでなければテストケースをスキップ

テストケースのマーキング方法:

@tag("level1")
def test_a(self):
    pass

この方法では、最終的なレポートに多くのskippedテストケースが表示され、環境などの他の理由でスキップが必要なテストケースに干渉する可能性があります。 ここでは、テストケースメソッドのdocstringに特定のタグを追加してTestSuiteを再構成する方法を実装します。 run.pyに以下を追加します:

def makesuite_by_tag(tag):
    suite = unittest.TestSuite()
    for case in collect():
        if case._testMethodDoc and tag in case._testMethodDoc:  # テストケースメソッドにdocstringがあり、docstringに指定タグが含まれる場合
            suite.addTest(case)
    return suite

テストケースのマーキング方法:

class TestUserLogin(BaseCase):
    def test_user_login_normal(self):
        """レベル1:正常なログイン"""    # レベル1はタグで、docstringのどこにでも配置可能
        case_data = self.get_case_data("test_user_login_normal")
        self.send_request(case_data)

前回失敗したテストケースの再実行

毎回実行後、結果から失敗したテストケースを取得し、TestSuiteに組み立てて指定ファイルにシリアライズします。rerun-fails時には、前回実行で失敗したTestSuiteを逆シリアライズして実行します。 run.pyに以下を追加します:

import pickle
import sys

def save_failures(result, file):   # fileはシリアライズ保存ファイル名、config/config.pyで設定
    suite = unittest.TestSuite()
    for case_result in result.failures:   # TestSuiteを組み立て
        suite.addTest(case_result[0])   # case_resultはタプルで、最初の要素はテストケースオブジェクト、後は失敗原因など

    with open(file, 'wb') as f:
        pickle.dump(suite, f)    # 指定ファイルにシリアライズ

def rerun_fails():  # 失敗テストケース再実行メソッド
    sys.path.append(test_case_path)   # テストケースパスをパッケージ検索パスに追加しないと、逆シリアライズしたTestSuiteがテストケースを見つけられない
    with open(last_fails_file, 'rb') as f:
        suite = pickle.load(f)    # 逆シリアライズしてTestSuiteを取得
    run(suite)

run.pyrun()メソッドを変更し、実行後に失敗テストケースのシリアライズファイルを保存します:

def run(suite):
    logging.info("================================== テスト開始 ==================================")

    with open(report_file, 'wb') as f: 
        # 結果をresult変数に格納
        result = HTMLTestRunner(stream=f, title="APIテスト", description="テスト説明", tester="テスター").run(suite)  

    if result.failures:   # 失敗テストケースのシリアライズファイルを保存
        save_failures(result, last_fails_file)

    # send_email(report_file)  # 設定ファイルから読み込み
    logging.info("================================== テスト終了 ==================================")

コマンドライン引数の使用

コマンドライン引数はrun.py(実行エントリーファイル)を呼び出す際に渡すパラメータで、python run.py --collect-onlyのように異なるパラメータで異なる実行戦略を実行します。 optparserでコマンドライン引数を実装します: config/config.pyに以下を追加します:

# コマンドラインオプション
parser = OptionParser()

parser.add_option('--collect-only', action='store_true', dest='collect_only', help='すべてのテストケースを一覧表示のみ')
parser.add_option('--rerun-fails', action='store_true', dest='rerun_fails', help='前回失敗したテストケースを実行')
parser.add_option('--testlist', action='store_true', dest='testlist', help='test/testlist.txtリストで指定されたテストケースを実行')

parser.add_option('--testsuite', action='store', dest='testsuite', help='指定されたTestSuiteを実行')
parser.add_option('--tag', action='store', dest='tag', help='指定されたタグのテストケースを実行')

(options, args) = parser.parse_args()  # オプションを適用
  • '--collect-only'は引数名、dest='collect_only'はoptions.collect_only変数に格納することを示し、'store_true'はこの引数がある場合、options.collect_only=Trueを意味します
  • 'store'は--testsuite='smoke_suite'のように、引数の値'smoke_suite'をoptions.testsuite変数に格納することを示します

コマンドラインオプションの使用方法: run.pyに以下を追加:

from config.config import *

def main():
    if options.collect_only:    # --collect-onlyパラメータが指定された場合
        collect_only()
    elif options.rerun_fails:    # --rerun-failsパラメータが指定された場合
        rerun_fails()
    elif options.testlist:    # --testlistパラメータが指定された場合
        run(makesuite_by_testlist(testlist_file))
    elif options.testsuite:  # --testsuite=***が指定された場合
        run_suite(options.testsuite)
    elif options.tag:  # --tag=***が指定された場合
        run(makesuite_by_tag(options.tag))
    else:   # それ以外はすべてのテストケースを実行
        run_all()

if __name__ == '__main__':
    main()   # main()を呼び出し

実行結果:

C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --collect-only
1.user.test_user_login.TestUserLogin.test_user_login_normal
2.user.test_user_login.TestUserLogin.test_user_login_password_wrong
3.user.test_user_reg.TestUserReg.test_user_reg_exist
4.user.test_user_reg.TestUserReg.test_user_reg_normal
----------------------------------------------------------------------
4件のテストケースを0.006秒で収集
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --rerun-fails
.
経過時間: 0:00:00.081812
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --testlist
..
経過時間: 0:00:00.454654
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --testsuite=smoke_suite
..
経過時間: 0:00:00.471255
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --tag=level1
.
経過時間: 0:00:00.062273
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py 
....
経過時間: 0:00:00.663564

その他の最適化

  1. 日別ログ生成、毎回実行時に新しいレポートを作成

config/config.pyを変更

import time

today = time.strftime('%Y%m%d', time.localtime())
now = time.strftime('%Y%m%d_%H%M%S', time.localtime())

log_file = os.path.join(prj_path, 'log', 'log_{}.txt'.format(today))  # パスをlogディレクトリに変更
report_file = os.path.join(prj_path, 'report', 'report_{}.html'.format(now))  # パスをreportディレクトリに変更
  1. send_email()スイッチの追加 config/config.pyに追加
send_email_after_run = False

run.pyを変更

from config.config import *

def run(suite):
    logging.info("================================== テスト開始 ==================================")

    with open(report_file, 'wb') as f:  # 設定ファイルから読み込み
        result = HTMLTestRunner(stream=f, title="APIテスト", description="テスト説明", tester="テスター").run(suite)

    if result.failures:
        save_failures(result, last_fails_file)

    if send_email_after_run:  # メールを送信するかどうか
        send_email(report_file)  
    logging.info("================================== テスト終了 ==================================")

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

6月4日 16:43 投稿