QAエンジニアがGeminiとペアプロしてGASでテスト業務を効率化した

はじめに

こんにちは!QAエンジニアの松永です。

QAの業務は多岐に渡りますが、その中の一つとしてテスト設計があります。

テスト設計という業務の中にはテスト仕様書の作成があり、使用するツールとしてGoogleスプレッドシートを採用しているのですが、テスト設計という作業の中にはどうしても避けられない「地味に面倒な作業」が数多く存在します。

  • 面倒な作業の一例
    • 準備の面倒: 新しいテストを作成するたびに元となるテスト仕様書をコピーして、命名規則に沿ってリネーム
    • 入力の面倒:テスト手順を1つのセル内に箇条書きで書き、各行の先頭に「1. 」「2. 」と手入力し、手順変更のたびにそれらを手で修正するという作業が発生
    • 更新の面倒: テストケースの行を1つ追加/削除したら、以降の項目番号全部修正
    • 記録の面倒: 誰がいつテストしたのか、担当者と日付を毎回ポチポチ入力
    • 集計の面倒: 複数あるテスト仕様書のシートから、全体の進捗を手作業で転記・集計

これらの単純作業を自動化し、もっと本質的なテスト設計や分析に集中したいと考え、Geminiを相棒にGoogleAppsScriptでの自動化に挑戦しました。

GoogleAppsScriptGAS)とは
Googleが提供する各種Googleサービスを連携・自動化するためのプログラミング環境Gmailやスプレッドシートなどのサービスを連携・自動化ができる

Google Apps Script: Google Workspace を自動化、統合、拡張。

完成した「自動化機能」の全体像

今回作成したスクリプト

  1. 【準備の自動化】 ワンクリックでテスト仕様書シートを作成する機能
  2. 【入力のアシスト】 リアルタイムで手順セル内の自動ナンバリング
  3. 【集計の自動化】 全シートの状況を瞬時に可視化する機能

各機能のポイントを解説

  • 機能①:準備の自動化

    • 前提:ファイルは以下の画像の構成になっており、テスト設計をする際はマスターシートである[master]を複製して設計をしている

    • Before:

      • 手動で[master]を複製 → 右クリックで名称変更 → 新しい名前を入力の繰り返し
    • After:
      • スクリプトを実行 → ポップアップにシート名前を全て入力 → 完了 youtu.be
      • 補足:[_シナリオテスト仕様書]はデフォルトで命名するように実装
    • ポイント:
      • エラーを防ぐためにマスターシートの存在確認や、作成済みのシート名との重複チェックも追加。
  • 機能②:手順セル内の自動ナンバリング

    • Before:
      • 手動で手順番号入力→手順の変更を行う→セル内の手順番号をすべて振り直し
    • After:
      • 手順の変更を行う→自動的に完了 youtu.be
    • ポイント:
      • セル内で手順を改行して入力するだけで、「1.」「2.」といった連番が自動で付与・整理されるため、手入力による番号の修正は不要。
  • 機能③:集計の自動化

    • Before:
      • 全てのテスト仕様書シートの集計表からセル参照で値を取得、シートが増減すると合計・進捗周りの数式を修正。
      • 手作業のためミスも多い。
    • After:
      • スクリプトを実行するだけで、全シートへのリンクと最新の集計結果が一覧になったシートが完成。  youtu.be
    • ポイント:
      • 条件に合致するシートから、必要な情報だけを抜き出して一覧化する集計シートを全自動で作成。
      • 自動集計の仕組みは、GASが書き出したシート名の一覧を元に、INDIRECT関数で各シートの値を自動で参照しているため、常に最新の状態が保てる。

Webサイトで見かけたアイデアを、Geminiと「自分のもの」にした方法

  • Step 1:ヒントの発見 💡

Webサイトで「セル内の改行テキストに連番を振る」GASのアイデアを見つけ、設計が楽になると直感しました。しかし、コードをそのまま流用はできなかったため、Geminiに聞きながら1から作り直すことにしました。

  • Step 2:Geminiとのペアプロ開始 💬

まず、やりたいことの核を、平易な日本語でGeminiに伝えます。

自分:「GASで、特定のセルに書かれた文字列に改行があれば、各行の先頭に『1.』『2.』といった連番を付けたい」

  • Step 3:対話によるコードの進化 🤖✨

Geminiが提示した基本的なコードを元に、理想の形になるまで対話を繰り返します。

自分:「ありがとう!でも、すでにある『1.』みたいな番号は、一旦全部消してから振り直したいな」 Gemini:(正規表現 replace を使って、既存の番号をリセットするコードを提案)

自分:「じゃあ、この処理を『手順』という見出しの列だけで動くようにしたいんだけど」 Gemini:(createTextFinder を使って、ヘッダーの位置を探し、編集された列が一致するか判定するコードを提案)

自分:「最後に、セルの内容が変わらない時は、無駄な書き込み処理をスキップして軽くしたい」 Gemini:(処理前後の文字列を比較する if 文を提案)

上記はやり取りの中のほんの一部です。

このようにGeminiは単に答えをくれるだけでなく、対話を通じて思考を整理しコードを改善してくれる最高の壁打ち相手になってくれました。

得られた成果

  • 単純作業から解放され、精神的な負担が劇的に減少。
  • 手作業によるミスがなくなり、テスト仕様書の品質が向上。
  • テスト仕様書のメンテナンスコストが下がったことで、より気軽に内容の改善に取り組めるようになった。

最後に

今回、GASとGeminiを使って、日々の面倒な業務を自動化してみました。

具体的な削減時間の計測はしていませんが、それ以上に「連番ズレを気にするストレス」や「単純作業の煩わしさ」から解放された精神的なメリットが非常に大きかったです。

スプレッドシートが賢くなっていくことで、テスト仕様書をメンテナンスするコストが下がり、もっと気軽に内容の改善に取り組めるようになりました。まさに「テスト仕様書を育てる」という感覚です。

この記事が、同じように日々の面倒な作業に悩む方の、はじめの一歩に繋がれば嬉しいです。


【参考】今回Geminiと作成したコード

機能②:手順セル内の自動ナンバリングのコード

/**
 * シート編集時に自動実行される関数。
 * 編集されたのが複数セルの場合は処理を中断し、手順列の自動採番処理を呼び出す。
 * @param {Object} e - Google Sheetsから渡されるイベントオブジェクト
 */
function onEdit(e) {
  // 複数のセルが同時に編集された場合は処理を中断
  if (e.range.getNumRows() > 1 || e.range.getNumColumns() > 1) {
    return;
  }

  // ヘルパー1:手順列の自動採番処理を呼び出す
  // 必要な情報をオブジェクトとして渡す
  const eventData = {
    range: e.range,
    editedRow: e.range.getRow(),
    e: e 
  };
  _handleProcedureNumbering(eventData);
}

/**
 * ヘルパー1:「手順」列のセルが編集されたら、そのセル内の各行に連番を自動で振る
 * @param {Object} eventData - onEditから渡されるイベント情報オブジェクト
 */
function _handleProcedureNumbering({ range, editedRow, e }) {
  const sheet = e.source.getActiveSheet();
  // 1. まず、編集されたセルの上にあるはずのヘッダーを探す(基準:'手順')
  const headerInfo = _findHeaderInfo(sheet, editedRow - 1, '手順');
  if (!headerInfo) return; // ヘッダーが見つからなければ終了
  
  // 2. ヘッダー情報を使って、「手順」列が編集されたかを正確に判断
  const procedureCol = headerInfo.headerMap.get('手順');
  if (range.getColumn() !== procedureCol || editedRow <= headerInfo.headerRow) return;

  // 3. セルの値を取得し、中身がなければ何もしない
  const cellText = range.getValue().toString();
  if (cellText.trim() === '') return;

  // 4. 既存の行番号を消去し、改行で区切って配列にする
  const lines = cellText.replace(/^\d+\.\s*/gm, '').split('\n').map(line => line.trim()).filter(Boolean);
  if (lines.length === 0) {
    range.clearContent();
    return;
  }

  // 5. 新しい連番を付けてテキストを再生成し、変更があった場合のみセルに書き込む
  const newText = lines.map((line, index) => `${index + 1}. ${line}`).join('\n');
  if (newText !== cellText) {
    range.setValue(newText);
  }
}

/**
 * 編集された行から上方向に遡ってヘッダー行を探し、その情報をオブジェクトとして返す。
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet - 対象シート
 * @param {number} startRow - 検索を開始する行番号(通常は編集された行)
 * @param {string} keyHeader - ヘッダー行を特定するための必須の列見出し(例: '手順')
 * @return {object|null} - 見つかった場合: {headerRow: number, headers: string[], headerMap: Map} or null
 */
function _findHeaderInfo(sheet, startRow, keyHeader) {
  // 編集行までのデータのみを一度に読み込むことで、処理を高速化
  if (startRow < 1) return null; 
  const data = sheet.getRange(1, 1, startRow, sheet.getLastColumn()).getValues();
  // 編集行から1行目に向かってループ
  for (let i = startRow - 1; i >= 0; i--) {
    const rowData = data[i];
    // 行データに必須ヘッダーが含まれているかチェック
    if (rowData.includes(keyHeader)) {
      // 見出し名をキー、列番号を値とするMapオブジェクトを作成(高速な検索のため)
      const headerMap = new Map();
      rowData.forEach((header, index) => {
        if (header) headerMap.set(header, index + 1);
      });
      // 必要な情報をまとめてオブジェクトで返す
      return {
        headerRow: i + 1,   // ヘッダーの行番号 (1から始まる)
        headers: rowData,   // ヘッダー文字列の配列
        headerMap: headerMap // 見出し名と列番号の対応表
      };
    }
  }
  return null; // 最後まで見つからなければnullを返す
}