テストケースの基礎クラスの活用
各テストケースではExcelからデータを読み込み、データを解析、リクエストを送信、レスポンス結果を検証する必要があります。これらの共通処理をBaseCaseという基礎クラスに封装し、テストケースの記述を簡略化できます。
testディレクトリを再編成し、ケースファイルをtest/caseフォルダに格納し、カスタム
TestSuiteをtest/suiteフォルダに格納します。test_user_data.xlsxにdata_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.pyをrun.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.pyのrun()メソッドを変更し、実行後に失敗テストケースのシリアライズファイルを保存します:
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
その他の最適化
- 日別ログ生成、毎回実行時に新しいレポートを作成
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ディレクトリに変更
- 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("================================== テスト終了 ==================================")