Googleフォームを送信しなくてもGASをデバッグできるようにする

# はじめに
本記事は以下のような方の役に立つはずです。
**・GoogleフォームのGASをテスト/デバッグするときに毎回フォーム送信するのが手間だと思っている人**
**・スクリプトエディタでデバッグ実行し、ブレークポイントを置きながらテストしたい人**
**・エラーになったとき、フォームを再送信してもらうのが難しいので、回答スプレッドシートに書き込まれたデータでリトライしたい人**


# 前提
以下の関数が、**Form回答スプレッドシート内のスクリプトエディタ**でトリガー設定されているとします。
```javascript
function formTriggerFunction(event) {

  // スプレッドシートに書き込まれた行番号を取得する
  const writeRowNum = event.range.getLastRow();
  console.info(`${writeRowNum}行目のデータを処理します。`);

  // namedValuesを取得する
  const namedValues = event.namedValues;
  console.info(namedValues);

}
```

### トリガー設定方法

https://qiita.com/hajimejimejime/items/33d489a573afc75bc6b7#form%E5%9B%9E%E7%AD%94%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E3%82%B7%E3%83%BC%E3%83%88%E5%86%85%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF




# トリガー実行時にeventが関数に渡される
フォームが送信されると自動的にフォームの回答などの情報が入った```event```オブジェクトを引数として渡し、関数が実行されます。

Formの内容は ```namedValues``` オブジェクトに
key: **Formの質問名**
Value: **Formの入力値の配列**
の形式で入っています。

以下のように入力して送信すると
![form.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557347/5c2cbe11-2bea-d4a6-369d-01e50d56f343.jpeg)


このような namedValues オブジェクトになります。
```javascript
// event.namedValues
{ 
  'ラジオボタン': [ '選択肢 1' ],
  '記述_短文': [ '回答' ],
  'タイムスタンプ': [ '2025/01/20 22:34:38' ],
  'チェックボックス': [ '選択肢 2, その他項目' ]
}
```

回答の取得は
```javascript
const timeStamp = event.namedValues["タイムスタンプ"][0];
console.info(timeStamp); //'2025/01/19 1:09:38'
```
のようにすると取得できます。

# スクリプトエディタからの実行ではeventが渡されないのでエラーになる
引数が設定されず```Undefined```になってしまうためエラーになってしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557347/76dae582-dcff-62e5-f306-a987f94ecd77.png)

こうなると、**テストのたびにフォームを送信することになり手間**です。
そして、**トリガーでの実行だと、ブレークポイントを置いて処理を止めながらデバッグすることが出来ない**という問題があります。

# 手動でevent引数を再現する
このような問題を解決する方法として
**トリガーで自動設定される ```event``` を回答スプレッドシートに書き込まれたデータから再現する。**
という方法で解決していきます。

ここでは
・フォームの内容が入っている **```namedValues```**
・回答スプレッドシートに書き込んだ範囲 **```range```**
の2項目のみを再現します。

他の項目は公式ドキュメントに記載があります。(再現できるかは未検証です...)

https://developers.google.com/apps-script/guides/triggers/events?hl=ja#form-submit

## 再現するコード
```javascript
function reproductionEvent(targetRow) {

  // スプレッドシート、シート取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("フォームの回答 1");

  // 最終列取得
  const lastColumn = sheet.getLastColumn();

  // 質問を取得
  const questionValues = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0];

  // 回答が書き込まれた範囲を取得
  const range = sheet.getRange(targetRow, 1, 1, lastColumn);
  // 回答を取得
  const anserValues = range.getDisplayValues()[0];

  // namedValuesを再現する
  const namedValues = {};
  for (const index in questionValues) {
    namedValues[questionValues[index]] = [anserValues[index]];
  }

  // eventを再現する(namedValues, rangeのみ)
  const event = {namedValues, range};

  // eventを返す
  return event;

}
```

### 処理内容
回答スプレッドシートの
**1行目の質問名をkey**
**指定した行のデータをvalue**
にして ```namedValues``` を再現しています。

以下のような回答スプレッドシートだったら
|1| タイムスタンプ | 記述_短文 | ラジオボタン | チェックボックス |
|:-:|:-:|:-:|:-:|:-:|
|2|2025/01/20 23:18:57|回答|選択肢 1|選択肢 1, その他手入力|
|3|2025/01/20 23:19:11|回答2|選択肢 1||

■ targetRow「2」を指定した場合 
```javascript
{ 
  'タイムスタンプ': [ '2025/01/20 23:18:57' ],
  '記述_短文': [ '回答' ],
  'ラジオボタン': [ '選択肢 1' ],
  'チェックボックス': [ '選択肢 1, その他手入力' ]
}
```
■ targetRow「3」を指定した場合
```javascript
{ 
  'タイムスタンプ': [ '2025/01/20 23:19:11' ],
  '記述_短文': [ '回答2' ],
  'ラジオボタン': [ '選択肢 1' ],
  'チェックボックス': [ '' ]
}
```

このため、**過去のデータをリトライするということも出来ます。**

## 使い方
以下のようにするとeventを再現後引数に設定し関数が実行されます。
```javascript
function manualRun() {

  // 行数を指定
  const targetRow = 3;

  // eventを再現する
  const event = reproductionEvent(targetRow);

  // 処理を実行
  formTriggerFunction(event);

}
```
無事Formトリガーを設定している関数がスクリプトエディタから実行されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/557347/a2eca2a1-98ef-6eeb-aa3f-51d7ca4d400d.png)



# コード全体
```javascript
// Form送信時トリガーを設定する関数
function formTriggerFunction(event) {

  // スプレッドシートに書き込まれた行番号を取得する
  const writeRowNum = event.range.getLastRow();
  console.info(`${writeRowNum}行目のデータを処理します。`);

  // namedValuesを取得する
  const namedValues = event.namedValues;
  console.info(namedValues);

}

// event を再現する関数
function reproductionEvent(targetRow) {

  // スプレッドシート、シート取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("フォームの回答 1");

  // 最終列取得
  const lastColumn = sheet.getLastColumn();

  // 質問を取得
  const questionValues = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0];

  // 回答が書き込まれた範囲を取得
  const range = sheet.getRange(targetRow, 1, 1, lastColumn);
  // 回答を取得
  const anserValues = range.getDisplayValues()[0];

  // namedValuesを再現する
  const namedValues = {};
  for (const index in questionValues) {
    namedValues[questionValues[index]] = [anserValues[index]];
  }

  // eventを再現する(namedValues, rangeのみ)
  const event = {namedValues, range};

  // eventを返す
  return event;

}


// 手動実行用関数
function manualRun() {

  // 行数を指定
  const targetRow = 3;

  // eventを再現する
  const event = reproductionEvent(targetRow);

  // 処理を実行
  formTriggerFunction(event);

}
```