ジャンルから探す

Python English

Macで動作するペアワイズテスト生成GUIツールを作ってみた

ペアワイズ法のテストケースを、GUIで入力して CSV / Excel に書き出せるツールを Python で作りました。Mac でも Windows でも動きます。

PICT のようなコマンドラインツールは強力ですが、入力ファイルを書いて実行するという手順が、非エンジニアには壁になります。パラメータをフォームに入れてボタンを押すだけ、という形にしたのが今回の狙いです。

この記事は2025年の公開後、2026年9月に見直しました。 「カバレッジ率」という機能について、値を下げるとペアワイズの網羅性が失われるという重要な注意点が抜けていたため追記しています。

スポンサーリンク

作ったもの

GUI Tkinter(Python標準)
ペアワイズ生成 allpairspy
データ処理 pandas
Excel入出力 openpyxl
対応OS macOS / Windows

できることは次の5つです。

  • パラメータと値をフォームで追加・削除する
  • ペアワイズのテストケースを生成する
  • 生成件数を絞り込む(注意点あり。後述します)
  • 結果を CSV / Excel に書き出す
  • 既存の CSV / Excel を読み込んでパラメータを復元する

動作画面

パラメータごとに「値1, 値2, …」とカンマ区切りで入力し、「テストケース生成」を押すと結果が表に出ます。

スポンサーリンク

ソースコード

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
from allpairspy import AllPairs
import openpyxl

class MiniPictApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("MiniPict - ペアワイズテスト生成ツール")
        self.geometry("700x650")

        self.param_entries = []
        self.create_widgets()

    def create_widgets(self):
        # パラメータ入力欄ラベル
        lbl = tk.Label(self, text="パラメータと値を入力(カンマ区切り)")
        lbl.pack(pady=10)

        # スクロール可能なFrame
        frame = tk.Frame(self)
        frame.pack(expand=True, fill='both', padx=10)

        canvas = tk.Canvas(frame)
        scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
        scrollable_frame = tk.Frame(canvas)

        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )

        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        self.scrollable_frame = scrollable_frame

        # ボタンたち
        button_frame = tk.Frame(self)
        button_frame.pack(pady=10)

        add_btn = ttk.Button(button_frame, text="パラメータを追加", command=self.add_parameter)
        add_btn.pack(side='left', padx=5)

        generate_btn = ttk.Button(button_frame, text="テストケース生成", command=self.generate_test_cases)
        generate_btn.pack(side='left', padx=5)

        save_btn = ttk.Button(button_frame, text="CSVとして保存", command=self.save_to_csv)
        save_btn.pack(side='left', padx=5)

        save_excel_btn = ttk.Button(button_frame, text="Excelとして保存", command=self.save_to_excel)
        save_excel_btn.pack(side='left', padx=5)

        import_btn = ttk.Button(button_frame, text="CSV/Excelのインポート", command=self.import_values)
        import_btn.pack(side='left', padx=5)

        # カバレッジ入力欄
        self.coverage_label = tk.Label(self, text="カバレッジ(手動入力):")
        self.coverage_label.pack(pady=10)

        self.coverage_entry = ttk.Entry(self, width=20)
        self.coverage_entry.pack(pady=5)

        self.result = None

    def add_parameter(self):
        frame = tk.Frame(self.scrollable_frame)
        frame.pack(pady=5, anchor='w')

        label_entry = ttk.Entry(frame, width=20)
        label_entry.insert(0, f"パラメータ{len(self.param_entries)+1}")
        label_entry.pack(side="left", padx=5)

        value_entry = ttk.Entry(frame, width=50)
        value_entry.insert(0, "値1, 値2, 値3")
        value_entry.pack(side="left", padx=5)

        delete_btn = ttk.Button(frame, text="削除", command=lambda: self.delete_parameter(frame))
        delete_btn.pack(side="left", padx=5)

        self.param_entries.append((label_entry, value_entry, frame))

    def delete_parameter(self, frame):
        # パラメータ削除処理
        for entry in self.param_entries:
            if entry[2] == frame:  # 対象のフレームを見つけたら
                self.param_entries.remove(entry)  # リストから削除
                break
        frame.destroy()  # フレームを削除

    def generate_test_cases(self):
        try:
            parameters = []
            headers = []

            for label_entry, value_entry, _ in self.param_entries:
                name = label_entry.get()
                values = [v.strip() for v in value_entry.get().split(",") if v.strip()]
                if not values:
                    raise ValueError(f"{name} の値が空です")
                headers.append(name)
                parameters.append(values)

            # カバレッジ率の入力と検証
            manual_coverage = self.coverage_entry.get().strip()
            if manual_coverage:
                try:
                    manual_coverage = float(manual_coverage)
                    if not (0 <= manual_coverage <= 100):
                        raise ValueError("カバレッジは0から100の範囲で入力してください")
                except ValueError:
                    messagebox.showerror("エラー", "カバレッジは0から100の数字で入力してください")
                    return
            else:
                manual_coverage = 100  # デフォルトで100%カバレッジ

            # ペアワイズ組み合わせの生成
            all_pairs = list(AllPairs(parameters))
            total_combinations = len(all_pairs)

            # 必要なテストケースの数
            if manual_coverage == 100:
                pairs = all_pairs  # 100%カバレッジの場合、全ての組み合わせ
            else:
                required_cases = int(total_combinations * (manual_coverage / 100))
                pairs = all_pairs[:required_cases]

            # DataFrameの作成
            df = pd.DataFrame(pairs, columns=headers)

            # パラメータ1、パラメータ2、パラメータ3の順で並び替え
            df = df.sort_values(by=headers[:len(parameters)], ascending=True).reset_index(drop=True)

            self.result = df

            # 結果表示(ダイアログ)
            preview = "\n".join(df.head(10).astype(str).agg(" | ".join, axis=1).tolist())
            messagebox.showinfo("テストケース生成結果(先頭10件)", preview)

        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def save_to_csv(self):
        if self.result is None:
            messagebox.showwarning("警告", "まずはテストケースを生成してください。")
            return

        file_path = filedialog.asksaveasfilename(defaultextension=".csv",
                                                 filetypes=[("CSVファイル", "*.csv")])
        if file_path:
            self.result.to_csv(file_path, index=False)
            messagebox.showinfo("保存完了", f"{file_path} に保存しました。")

    def save_to_excel(self):
        if self.result is None:
            messagebox.showwarning("警告", "まずはテストケースを生成してください。")
            return

        file_path = filedialog.asksaveasfilename(defaultextension=".xlsx",
                                                 filetypes=[("Excelファイル", "*.xlsx")])
        if file_path:
            with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
                self.result.to_excel(writer, index=False, sheet_name='Test Cases')
            messagebox.showinfo("保存完了", f"{file_path} に保存しました。")

    def import_values(self):
        file_path = filedialog.askopenfilename(filetypes=[("CSVファイル", "*.csv"),
                                                          ("Excelファイル", "*.xlsx")])
        if not file_path:
            return

        try:
            if file_path.endswith(".csv"):
                data = pd.read_csv(file_path)
            else:
                data = pd.read_excel(file_path)

            # インポートした値でパラメータを更新
            for idx, col in enumerate(data.columns):
                if idx >= len(self.param_entries):
                    self.add_parameter()
                self.param_entries[idx][1].delete(0, tk.END)
                self.param_entries[idx][1].insert(0, ", ".join(data[col].dropna().astype(str).tolist()))

            messagebox.showinfo("インポート完了", "ファイルのインポートが完了しました。")
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルのインポートに失敗しました: {str(e)}")

if __name__ == "__main__":
    app = MiniPictApp()
    app.mainloop()

コードの解説

GUI(Tkinter)

パラメータは何個でも追加できるようにしたいので、Canvas と Scrollbar を組み合わせてスクロール可能な領域を作っています。

  • Frame / Canvas / Scrollbar でスクロール領域を構成
  • パラメータ1行につき Entry(名前と値)を2つ配置
  • 各行に削除ボタンを付けて、あとから減らせるようにする

Tkinter は Python 標準ライブラリですが、Homebrew の Python では別途 python-tk が必要です。動かない場合は_tkinterモジュールが見つからないエラーの対処方法を参照してください。

ペアワイズ生成(allpairspy)

生成そのものは1行です。

from allpairspy import AllPairs
...
all_pairs = list(AllPairs(parameters))

parameters は「値のリスト」のリストです。たとえば次の形になります。

parameters = [
    ["Windows", "macOS", "iOS"],
    ["Chrome", "Safari"],
    ["Free", "Paid"],
]

for case in AllPairs(parameters):
    print(case)

並び順の制御

生成結果は、入力したパラメータの順序で並べ替えています。

df = df.sort_values(by=headers[:len(parameters)], ascending=True).reset_index(drop=True)

これがないと結果の並びが読みづらく、レビューしにくくなります。

CSV / Excel の入出力

pandas に任せています。

  • CSV保存:to_csv()
  • Excel保存:to_excel()(openpyxl が必要)
  • 読み込み:read_csv() / read_excel()

Excelで受け渡しできるようにしたのは、テスト設計をエンジニア以外と共有する場面が多いためです。

スポンサーリンク

「カバレッジ率」を下げると網羅性は失われる

ここが最も重要な注意点です。公開当時の記事では説明が足りていませんでした。

このツールには生成件数を絞る機能があり、当時は「カバレッジ率」と呼んでいました。しかしこの数値を100未満にすると、ペアワイズの前提である「すべての2因子の組み合わせを1回は通す」が成立しなくなります。

設定 実際に起きること
100 ペアワイズとして正しい。全ペアを網羅する
100未満 生成結果の一部だけを残す。網羅されないペアが出る

「カバレッジ50%」は「網羅率が半分」という意味ではありません。 単に生成されたケースの前半だけを取り出しているだけで、どのペアが欠けるかは制御されていません。

使うなら、次のように割り切ってください。

  • 正式なテスト設計に使うなら必ず100
  • 100未満は「時間がないので一部だけ流す」ときの暫定手段。網羅の根拠にはならない
  • 削るなら、機械的に前半を取るよりリスクの高い組み合わせを残す方が合理的

ペアワイズの考え方そのものはペアワイズテストの基本と実践方法にまとめています。最小ケース数の下限は「値の数が多い2因子の積」なので、それを下回った時点で網羅できていないと判断できます。

実務で使うときの限界

作ってみて分かった、このツールで足りない点も書いておきます。

制約(あり得ない組み合わせ)を指定できない

実務ではここが一番のネックです。 「iOS × Internet Explorer」のような存在しない組み合わせを除外する仕組みがありません。

allpairspy 自体は filter_func で対応できるので、コードに手を入れれば実装可能です。

def is_valid(row):
    # 生成途中の行が渡されるので、長さを確認してから判定する
    if len(row) >= 2:
        os_name, browser = row[0], row[1]
        if os_name in ("iOS", "Android") and browser == "Edge":
            return False
    return True

for case in AllPairs(parameters, filter_func=is_valid):
    print(case)

filter_func には生成途中の不完全な行も渡されます。 インデックスに直接アクセスする前に長さを確認しないと IndexError になります。

必ず含めたい組み合わせを固定できない

PICT の seeding に相当する機能がありません。主要な利用パターンが生成結果に入らないことがあるので、出力を目視で確認する必要があります。

3因子以上の網羅に対応していない

PICT の /o:3 に相当する指定はできません。重要な機能で3因子の組み合わせを確認したい場合は、PICT を使ってください。

ライブラリの更新頻度

allpairspy の最新版は 2.5.1(2023年7月)で、活発に開発が続いているライブラリではありません。動作に問題はありませんが、長期的に業務の中心に据えるなら PICT のような実績あるツールの方が安心です。

実行方法

必要なライブラリ

pip install allpairspy pandas openpyxl

実行

python3 test.py

エラーが出て動かない場合は_tkinterモジュールが見つからないエラーの対処方法を確認してください。Homebrew の Python では python-tk の追加インストールが必要です。

まとめ

  • Tkinter + allpairspy + pandas で、GUIのペアワイズ生成ツールが作れる
  • CSV / Excel に対応させると、エンジニア以外とも設計を共有できる
  • 「カバレッジ率」を100未満にすると、ペアワイズの網羅性は失われる。 正式な設計では必ず100にする
  • 制約の指定は filter_func で実装できる。 ただし生成途中の行が渡される点に注意
  • 必ず含めたい組み合わせの固定と、3因子網羅には対応していない
  • 本格的に使うなら PICT。このツールは「手軽に試す」用途に向く

自分で作ってみると、ツールが何をしていて何をしていないかが分かるのが一番の収穫でした。生成結果をそのまま信じるのではなく、網羅できているかを自分で検算する視点が持てるようになります。