2023年9月22日に開催された株式会社MagicPod主催のテストケース管理ツール5社に聞く、おすすめテストケース管理術にTestRailベンダーとして登壇いたしました。
催しの中でTestRailとMagicPod(自動テスト), Jenkins(CIツール)を連携させた環境を用いてデモを行いましたので、今回はこの連携の中身についてご紹介します。
テスト管理をテスト自動化に組み込む意図
弊社テクマトリックスでは、かれこれ20年近くソフトウェア開発向けのテストツールを取り扱っています。コードを解析してバグを見つける静的解析ツールやUIテスト自動化ツール、ソフトウェアの構造を可視化する構造分析ツールなど、様々なアプローチでソフトウェアの品質向上を目的に活動してきました。ここ10年ほどはテスト自動化をテーマとして活動されているお客様も多く、Jenkinsを用いたクラウド上の開発環境の構築などもご提案する機会が増えました。
自動化されたテストの対極にあるのが手動テストです。手動テストはユーザビリティテストや探索的テストなどの観点も含まれるため、手動テストがなくなることはありません。自動化されたテストと手動テストは混在して開発チームに存在することになります。
テスト結果は自動/手動テスト、テスト内容ごとにばらばらに管理されているのが当たり前とされているように思います。しかし、リリースするソフトウェアは1つです。それであれば、そのソフトウェアのテスト結果はすべて1つにまとめられているのが正しい姿ではないかと感じます。
テスト管理をテスト自動化に組み込む意図は、自動化されたテストと手動テストの結果をまとめて管理/管理することで、開発ソフトウェアの状態を早く正しく把握できる仕組みを持つことです。
TestRailのMagicPod連携
動いている様子を動画として公開しています。各ツールやサービスの画面や動作は動画をご覧ください。
概要
連携の処理はPythonにて実装し、Jenkinsで実行しました。詳しい処理の流れや内容は後述します。
TestRailからはMagicPodの結果が以下のように確認できます。細かいことですが、MagicPodのリンクや画面ショット、テストにかかった時間が追加されています。
処理の流れ
処理はJenkinsのパイプラインで実行しています。作成したパイプラインに沿って、処理の流れを説明します。
Jenkinsの「Prepare」ステージ
TestRailにはMagicPodのテストに対応するテストケースをあらかじめ登録しておきます。その上で、テスト結果を登録するためのテストラン(テスト計画)を作成します。
Jenkinsの「Test」ステージ
以下の処理を3種類のブラウザごとに並列に実施します。
- MagicPodの一括テスト実行を開始
- MagicPodのテストが終了したら、結果を取得し、TestRailにテスト結果を登録
Pythonスクリプトの内容
TestRailの呼び出し
TestRailはREST APIによる呼び出しが可能です。 今回の連携では、テストケースからテストランを作成する処理とテスト結果を登録する処理を利用します。
- TestRailのAPI呼び出し
- APIにアクセスするためのAPIバインディングのサンプルが用意されています。
https://github.com/gurock/testrail-api/ - 今回はPythonから呼び出しています。
- APIにアクセスするためのAPIバインディングのサンプルが用意されています。
- 「Prepare」ステージ:テスト計画の作成
- 今回のデモでは、3種類のブラウザのテストを行うため、3つのテストランを作成します。この3つのテストランを1つのテスト計画としてまとめています。
以下はTestRailの用語を図でまとめたものです。
- 今回はあらかじめ作成しておいたTestRailのテストケース番号とブラウザの設定を固定値として指定しています。
- テストランは対象となるテストケースを選択し、テストランを作成します。名称は「作成時の日付+MagicPod Test」としています。
- 今回のデモでは、3種類のブラウザのテストを行うため、3つのテストランを作成します。この3つのテストランを1つのテスト計画としてまとめています。
import os import sys import base64 import json import requests import argparse from datetime import datetime from testrail import * class TestRailAPIWrapper: def __init__(self, base_url, user, password): self._client = APIClient(base_url) self._client.user = user self._client.password = password def add_plan(self, project_id, entries): # https://docs.testrail.techmatrix.jp/testrail/docs/702/api/reference/plans/ # POST index.php?/api/v2/add_plan/:project_id entries["name"] = datetime.now().strftime("%Y-%m-%d-%H-%M") + " MagicPod Test" response = self._client.send_post( 'add_plan/'+str(project_id),entries) return response TESTRAIL_TESTPLAN_ENTRY = { "name": "testplan_name", "entries": [ { "suite_id": 9, "include_all": True, "config_ids": [6,7,8], # 3種類のブラウザを利用するよう指定 "runs": [ { "include_all": True, "case_ids": [2312, 2313], # MagicPodのテストに対応したテストケースを指定 "assignedto_id": 1, "config_ids": [6] # Chrome }, { "include_all": True, "case_ids": [2312, 2313], "assignedto_id": 1, "config_ids": [7] # Firefox }, { "include_all": True, "case_ids": [2312, 2313], "assignedto_id": 1, "config_ids": [8] # Edge } ] }, ] } def prepare_testplan(): print("Preparing test plan") client = TestRailAPIWrapper(TESTRAIL_URL, TESTRAIL_USER, TESTRAIL_PASSWORD) response = client.add_plan(TESTRAIL_PROJECT_ID, TESTRAIL_TESTPLAN_ENTRY) print(json.dumps(response, indent=4)) with open(TESTRAIL_TESTPLAN_JSON_FILENAME, "w", encoding='utf-8') as file: file.write(json.dumps(response))
- 「Test」ステージ:テスト結果の登録
- 「Prepare」ステージで作成したテスト計画にテストに結果を登録します。
- 登録するテスト結果はMagicPodの結果と画面ショットのパスが含まれるJSON形式のファイルを読み込みます。
- テスト結果を追加した後、テストに画面ショットを添付ファイルとして追加します。
import os import sys import base64 import json import requests import argparse from datetime import datetime from testrail import * class TestRailAPIWrapper: def __init__(self, base_url, user, password): self._client = APIClient(base_url) self._client.user = user self._client.password = password def get_tests(self, run_id): # https://docs.testrail.techmatrix.jp/testrail/docs/702/api/reference/tests/ # GET index.php?/api/v2/get_tests/:run_id response = self._client.send_get( 'get_tests/'+str(run_id)) return response def add_result(self, test_id, entries): # https://docs.testrail.techmatrix.jp/testrail/docs/702/api/reference/results/ response = self._client.send_post( 'add_result/'+str(test_id),entries) return response def add_attachment(self, result_id, filename): # https://docs.testrail.techmatrix.jp/testrail/docs/702/api/reference/attachments/ # POST index.php?/api/v2/add_attachment_to_result/:result_id response = self._client.send_post( 'add_attachment_to_result/'+str(result_id),filename) return response return def add_result(json_filename): print(f"Adding result using JSON file: {json_filename}") if not os.path.exists(json_filename): print("Error: json file not found.") sys.exit(1) if not os.path.exists(TESTRAIL_TESTPLAN_JSON_FILENAME): print("Error: testplan file not found.") sys.exit(1) with open(TESTRAIL_TESTPLAN_JSON_FILENAME, "r") as f: testplan_data = json.load(f) print(json.dumps(testplan_data, indent=4)) with open(json_filename, "r") as f: magicpod_result_data = json.load(f) print(json.dumps(magicpod_result_data, indent=4)) client = TestRailAPIWrapper(TESTRAIL_URL, TESTRAIL_USER, TESTRAIL_PASSWORD) # testrun is_succeed = 0 testruns = testplan_data['entries'][0]['runs'] magicpod_type = magicpod_result_data['test_setting_name'] # Browser magicpod_results = magicpod_result_data['test_cases']['details'][0]['results'] for testrun in testruns: # testrunとmagicpodの結果をマッピング(ブラウザ名で特定)し、テストランIDを特定 if testrun['config'] == magicpod_type: testrun_id = testrun['id'] # テストランIDからテストを取得 tests = client.get_tests(testrun_id) # test for test in tests: for magicpod_result in magicpod_results: # magicpodの結果(name)とテストの名前を比較, 一致した場合、テスト結果を登録 if test['title'] == magicpod_result['test_case']['name']: # 登録用のデータ整形 if magicpod_result['status'] == "succeeded": status = 1 elif magicpod_result['status'] == "failed": status = 5 is_succeed = 1 started_at = datetime.fromisoformat(magicpod_result['started_at'][:-1]) if magicpod_result['finished_at'] == "": finished_at = datetime.fromisoformat(datetime.now().strftime("%Y-%m-%dT%H:%M:%S")) else: finished_at = datetime.fromisoformat(magicpod_result['finished_at'][:-1]) elapsed_seconds = str((finished_at - started_at).total_seconds()) + "s" comment = f"MagicPod URL:{magicpod_result_data['url']}" result_data = { "status_id": status, "comment": comment, "elapsed": elapsed_seconds, } # 登録 add_result_response = client.add_result(test['id'], result_data) print(json.dumps(add_result_response, indent=4)) add_attachment_to_result_response = client.add_attachment(add_result_response['id'], magicpod_result['screenshot']) print(json.dumps(add_attachment_to_result_response, indent=4)) return is_succeed
MagicPodの呼び出し
- MagicPodのテストを外部から一括実行する場合、以下の方法があります。今回はmagicpod-api-client形式での実行をラップする処理をPythonで実装しました。
- curl形式
- Invoke-RestMethod形式
- magicpod-api-client形式
- 「Test」ステージ:テストの一括実行、結果と画面ショットの取得
- magicpod-api-client形式で一括実行した際は同期処理で呼び出しを行い、テストの終了を待ちます。
- テスト終了後にMagicPodのテスト結果を取得するために、magicpod-api-client get-batch-run や magicpod-api-client get-screenshots を実行します。
- 画面ショットを取得する際はすべての画面ショットがzipファイルとして取得されるため、解凍と最後の画面ショットのファイルパスのみをテスト結果のJSONに追加しています。
- 最終的に、テスト結果+最後の画面ショットのパスが入ったJSON形式のデータをファイルに書き出します。このファイルをTestRailへの結果登録で利用しています。
import os import re import requests import sys import json import inspect import subprocess import zipfile import shutil class MagicpodApiClientWrapper: def __init__(self, secret_api_token, org_name, project_name, cmd_path, tmp_dir): self._secret_api_token = secret_api_token self._org_name = org_name self._project_name = project_name self._cmd_path = cmd_path self._tmp_dir = tmp_dir def _run_command(self, command): try: result = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, # テキストモードで出力を取得 check=True # エラーコードが非ゼロの場合に例外を発生させる ) return result.stdout, result.stderr except subprocess.CalledProcessError as e: return None, f"エラー: コマンドがエラーコード {e.returncode} で終了しました。" except FileNotFoundError: return None, "エラー: コマンドが見つかりませんでした。" def batch_run(self, setting): command = [ self._cmd_path, "batch-run", "-t", self._secret_api_token, "-o", self._org_name, "-p", self._project_name, "-S", str(setting) ] stdout, stderr = self._run_command(command) return stdout def get_latest_batch_number(self, test_setting_name): latest_number = 0 url = f"https://magic-pod.com/api/v1.0/{self._org_name}/{self._project_name}/batch-runs/" headers = { "Authorization": f"Token {self._secret_api_token}" } response = requests.get(url, headers=headers) if response.status_code == 200: result = response.json() for run in result['batch_runs']: if run['test_setting_name'] == test_setting_name: latest_number = run['batch_run_number'] break return latest_number def get_batch_run(self, batch_run_number): url = f"https://magic-pod.com/api/v1.0/{self._org_name}/{self._project_name}/batch-run/{batch_run_number}" headers = { "Authorization": f"Token {self._secret_api_token}" } response = requests.get(url, headers=headers) if response.status_code == 200: result = response.json() return result def get_screenshots(self, batch_run_number): temp_directory = self._tmp_dir + ORGANIZATION_NAME + "_" + PROJECT_NAME + "_" + str(batch_run_number) temp_zipfile = temp_directory + "/screenshots_" + str(batch_run_number) + ".zip" if not os.path.exists(temp_directory): os.makedirs(temp_directory) command = [ self._cmd_path, "get-screenshots", "-t", self._secret_api_token, "-o", self._org_name, "-p", self._project_name, "-b", str(batch_run_number), "-d", temp_zipfile ] stdout, stderr = self._run_command(command) filelist = [] with zipfile.ZipFile(temp_zipfile) as zf: filelist = zf.namelist() zf.extractall(temp_directory) return filelist def get_max_numbered_files(self, file_paths): # 各親フォルダごとに最も数字が大きいファイルのパスを格納するリスト max_numbered_file_paths = [] # 親フォルダごとにファイルをグループ化 grouped_files = {} for file_path in file_paths: folder_name = os.path.dirname(file_path) base_name = os.path.basename(file_path) # パスを正規化 folder_name = os.path.normpath(folder_name) if folder_name not in grouped_files: grouped_files[folder_name] = [] grouped_files[folder_name].append(base_name) # 各親フォルダ内のファイルで最も数字が大きいファイルを選択 for folder_name, file_names in grouped_files.items(): max_file = max(file_names, key=lambda x: int(os.path.splitext(x)[0])) max_numbered_file_path = os.path.join(folder_name, max_file) max_numbered_file_paths.append(max_numbered_file_path) return max_numbered_file_paths def update_testresults(self, json_data, file_paths): # ファイルパスからテストケースの情報を抽出し、辞書に格納 screenshot_array = [] for file_path in file_paths: # 正規表現を使用して数字と名前を抽出 match = re.search(r'(\d+)_(.+?)[/\\]', file_path) # / または \ にマッチ if match: screenshot = {} screenshot['number'] = match.group(1) screenshot['name'] = match.group(2) # パスを正規化 base_dir = self._tmp_dir + ORGANIZATION_NAME + "_" + PROJECT_NAME + "_" + str(json_data['batch_run_number']) screenshot['screenshot'] = os.path.normpath(base_dir + "/" + file_path) screenshot_array.append(screenshot) # 2つ目のJSONデータを更新 details = json_data.get("test_cases", {}).get("details", []) for result in details[0]['results']: number = result['test_case']['number'] name = result['test_case']['name'].replace(' ', '_') for screenshot in screenshot_array: if screenshot['number'] == str(number) and screenshot['name'] == name: result["screenshot"] = screenshot['screenshot'] return json_data def run_magicpod(test_setting, output_filename, temp_dir): if not temp_dir.endswith('/'): temp_dir += '/' client = MagicpodApiClientWrapper(SECRET_API_TOKEN, ORGANIZATION_NAME, PROJECT_NAME, MAGICPOD_API_CLIENT_PATH, temp_dir) # MagicPodテスト実行 client.batch_run(test_setting) # テスト結果取得 test_setting_name = next((item['name'] for item in TEST_SETTING_LIST if item['id'] == test_setting), None) latest_batch_number = client.get_latest_batch_number(test_setting_name) test_results = client.get_batch_run(latest_batch_number) screenshots = client.get_screenshots(latest_batch_number) # テスト結果加工 last_screenshots = client.get_max_numbered_files(screenshots) magicpod_result = client.update_testresults(test_results, last_screenshots) # 結果をファイルに保存 with open(output_filename, "w", encoding='utf-8') as file: file.write(json.dumps(magicpod_result))
- 出力されるJSON形式の内容の例です。MagicPodから取得したテスト結果に”screenshot”として最後の画面ショットのパスを追加しています。
{ "organization_name": "ORGANIZATION", "project_name": "PROJECT", "batch_run_number": 26, "test_setting_name": "Edge", "status": "failed", "status_number": 2, "started_at": "2023-09-07T15:14:42Z", "finished_at": "2023-09-07T15:16:02Z", "test_cases": { "succeeded": 1, "failed": 1, "total": 2, "details": [ { "pattern_name": "\u5b9f\u884c\u8a2d\u5b9a", "included_labels": [], "excluded_labels": [], "results": [ { "order": 1, "test_case": { "number": 1, "name": "TestRail\u30c7\u30e2\u30b5\u30a4\u30c8 \u30ed\u30b0\u30a4\u30f3", "url": "https://app.magicpod.com/ORGANIZATION/PROJECT/1/" }, "number": 1, "status": "succeeded", "started_at": "2023-09-07T15:14:46Z", "finished_at": "2023-09-07T15:15:13Z", "data_patterns": null, "screenshot": "temp\\ORGANIZATION_PROJECT_26\\ORGANIZATION_PROJECT_batch_run_26\\1_TestRail\u30c7\u30e2\u30b5\u30a4\u30c8_\u30ed\u30b0\u30a4\u30f3\\17.jpg" }, { "order": 2, "test_case": { "number": 2, "name": "TestRail\u30c7\u30e2\u30b5\u30a4\u30c8 \u30ed\u30b0\u30a4\u30f3\uff08\u5931\u6557\uff09", "url": "https://app.magicpod.com/ORGANIZATION/PROJECT/2/" }, "number": 2, "status": "failed", "started_at": "2023-09-07T15:15:13Z", "finished_at": "2023-09-07T15:16:01Z", "data_patterns": null, "screenshot": "temp\\ORGANIZATION_PROJECT_26\\ORGANIZATION_PROJECT_batch_run_26\\2_TestRail\u30c7\u30e2\u30b5\u30a4\u30c8_\u30ed\u30b0\u30a4\u30f3\uff08\u5931\u6557\uff09\\17.png" } ] } ] }, "url": "https://app.magicpod.com/ORGANIZATION/PROJECT/batch-run/26/" }
まとめ
ツール同士がオフィシャルにサポートされていたり、便利なプラグインが公開されていたりすれば、実装することなく連携ができます。しかし、各ツールのインターフェイスが公開されていれば、今回ご紹介したように簡単に連携させることができますし、自分のチームのニーズに合った情報を連携させたり、好きなタイミングで好きなツールで通知を行うことができ、柔軟性が増します。
自動テストの導入や改善を検討されている場合、テスト管理との連携を視野に入れて検討されることをおすすめします。テスト結果のフィードバック性能が大幅に改善します。
今回作成したスクリプトはデモ用のものですが、連携を試したい方がいらっしゃいましたら、TestRail製品窓口までお問い合わせください。
- Webフォームでのお問い合わせ:Webフォーム
- メールでのお問い合わせ:testrail-info@techmatrix.co.jp
関連する製品
テスト管理ツール TestRail
テストケースの管理やテスト結果の記録、チームでの情報共有など、Excelを使ったテスト管理の業務に限界を感じていませんか?TestRailはシンプルで使いやすいUIを提供し、テストにかかるさまざまな管理コストの削減に貢献します。
■ TestRailの特長 ■
- テストにさまざまな情報を関連づけて一元管理
- Webブラウザー上でテストケースを簡単に入力や編集可能
- テスト実施の準備と結果の共有が容易
- 進捗や比較などのレポートを提供
- 要件 / 課題管理ツールやテスト自動化ツールと連携
日本国内では、テスト管理にExcelを使っていたお客さまからの乗り換えが多く、Web上で完結するテスト管理を実現されています。
TestRail でテスト管理のお悩みを解決しませんか?