magicpod-analyzerで自動テスト結果を取得して、テスト結果表に転記するGASを作った話

# 概要
自動テスト、順調ですか？
MagicPodのテスト一括実行スケジューリング機能を利用していると、毎週決まった曜日の決まった時刻にテストを自動実行してくれますので、とても助かるのですが、一方で、何らかの理由でテスト失敗ということも時にはあります。
メール通知やSlack通知機能を使ってテスト結果の共有もできるのですが、例えば、傾向的に失敗するテストケースや、dev環境とstg環境での差異など、パッと見でなんらかの気づきを得たいと思うこともあるでしょう。
今回紹介するのは、[MagicPodの導入事例](https://magic-pod.com/customer-stories/locoguide/)で見かけた「magicpod-analyzer」を利用して、MagicPodのAPI経由で最新のテスト結果をJSON（JavaScript Object Notation）形式のデータで取得し、テスト結果表に転記するGAS（Google Apps Script）になります。
上記の導入事例では、BigQueryとダッシュボードを利用した事例ですが、こちらは、GASでチマチマとスプレッドシートを扱う事例になります。
# テストスケジュールとテスト結果表について
MagicPodのスケジュール設定機能は、こんな画面で実行する曜日と時刻を設定します。
![autoRec001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/eee27c15-f143-a230-4f26-a2c9f8aa743c.png)
例では月曜日と木曜日の、9:30に開始する設定の場合です。MagicPodの契約内容にも因るのですが、一度に使えるブラウザの数には限りがあり、またテストケースによっては実行時間を要するものがあったり、なるべく日中はテストステップの実装に環境を使いたいとか、Deployのタイミングでなるべく決まった曜日にe2eテストを実行したいとか、プロジェクト・メンバーと話をしている内に、手持ちのテストケースを、いつスケジュール実行するか段取りしていくと、あっという間にパツパツになったりもします。
そこで、一週間のテスト計画表を作成します。
![autoRec002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/ae7bc2c9-60c0-b828-3f5e-f291b286000a.png)
「何曜日の何時に、どのテストが走るのか」は、まとめておくことをお薦めします。
この例だと、平日の午後に空き時間があるので、そこで自動テストの実装と確認作業にMagicPodが利用できそうです。
さて、一週間の段取りが組み終わると、今度は自動テスト結果が、当然どんどん溜まっていきます。
中には、失敗するテストもあります。
これを、success / failed でテスト結果に記録していくのですが、毎日手動コピペしていると、とても大変な作業です。
![autoRec008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/fe6e3419-70ee-c872-1889-4750e6274121.png)
[magicpod-analyzer](https://github.com/takeya0x86/magicpod-analyzer)というツールが、Takeshi Kishi様より公開されていて、これを利用するとAPI経由でMagicPodのテスト結果をJSON形式で取得してくれますので、これを使います。
尚、MagicPodには、Web APIも用意されており、[こちら](https://magic-pod.com/api/v1.0/doc/)を利用することも可能ですが、magicpod-analyzerだと、ymlにプロジェクト名を複数記載しておけば、一発で全テスト結果を取得してくれます。
最後に取得したテスト一括実行番号を覚えていてくれるので、次回は未取得のデータのみ取得してくれます。
このlast_runの値は、時々データ取得が何故かできない時、記録されているテスト一括実行番号を進めて対処することもあり、ご参考になればと思います。
ymlの記述例。projects以下、一括取得したい自動実行テスト結果のプロジェクト名を記述して臨みます。

``` magicpod_analyzer.yaml
magicpod:
  projects:
    - MyOrganization/MyProjectOne //ここに、組織名/プロジェクト名1
    - MyOrganization/MyProjectTwo //ここに、組織名/プロジェクト名2
  exporter:
    local:
      format: json
  lastRunStore:
    backend: local

```
尚、テスト実施時刻は、30分単位で段取りしました。テスト実行時間が短いのもあると、テスト実行していない時間もできてしまうのですが、実行時間のかかるものもあるので（例えば、そういうのは3枠くらい確保する）そのようにしています。
尚、MagicPodのスケジュールは1分刻みに設定できます。
GASは、スプレッドシートの［拡張機能］-［Apps Script］でIDEが立ち上がりますので、GASのプログラミング開始。
![autoRec006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/17be852a-665e-3891-8786-2873fc7f2761.png)
mainは、
```
function main(){
  planSheetOpen(); //テスト計画シートを開いて
  var file = getFilesByNameRgeExp();
//記録に使うJSONファイルはスプレッドシートと同じGoogleDriveに置く
  Logger.log(file);
  resultArray = readJson(file); //JSONデータからテスト計画に記載のテストについて結果を取得し、
  resultSheetOpen(); //テスト結果シートを開いて
  recResult(resultArray); //取得したテスト結果を転記
}
```
IDEから実行します。
テスト結果の転記が終わったら、JSONデータはTrashフォルダに移動して、転記対象のJSONデータがスプレッドシートと同じフォルダに存在するようにします。
（なので、半自動 m(_ _)m ）
# 取得されるJSONについて
一部抜粋ですが、以下のようなJSONファイルがmagicpod-analyzerで取得できます。
```
[
  {
    "workflowId": " ここが組織名とプロジェクト名とテストケース名 ",
    "buildNumber":  ここに一括実行の番号 ,
    "workflowName": "test009",
    "createdAt": "2022-06-16T14:00:18.000Z",
    "branch": "",
    "service": "magicpod",
    "status": "SUCCESS",
    "successCount": 1
  },
```
workflowId に、前述の一週間のテスト段取り表に記載されている一括実行設定（例えば、設定40_test040）に含まれるテスト名が入りますので、これで実施予定時刻を探すことができますね。
1点注意なのは、createdAtは、UTCである点です。一週間のテスト段取り表はJSTで作成しているので9時間の補正をGASでします。
# テスト段取り表とテスト結果表の準備
Googleスプレッドシートにシートを2枚用意して、一週間のテスト段取り表を「テスト計画」シート、結果を記入するシートを「テスト結果」シートとします。
また、JSONデータを読んでcreatedAtとworflowNameを使ってシート中の検索を容易に行なうために、名前付き範囲をシートに定義しておきます。
![autoRec007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/0569dc61-97e1-9640-c30b-b442e276e1d1.png)
rangeMondayは、テスト計画の月曜日のテストだけ0:00から23:30までを含む範囲といった具合です。
* JSONデータからテスト実施日（CreateAt）を読んで、曜日を特定
* 曜日から検索範囲を指定
* テスト名をkeyにして検索し、一括実行開始時刻を調べる
# GASによるJSONデータの読み込みからテスト結果の記録まで
実は、GAS初めてです。。。
ExcelでVBAは経験ありましたが、GASを扱うのは初めてだったので、お作法に慣れるまでは若干の時間を費やしました。
でも、なんとかなります。
スプレッドシートとJSONデータは、同じGoogleドライブに置き、テスト結果の記録が済んだら廃棄します。
まず、JSONデータは、パーサーと呼ばれるGASの機能を使って読みます。変数 jobj に格納しています。

```
function readJson(file_name) {
  var fileIT = DriveApp.getFilesByName(file_name).next();
  var textdata = fileIT.getBlob().getDataAsString('utf8');
  var jobj = JSON.parse(textdata);

  var retArray = [];

//テスト名と実施日とテスト結果のJSONを受け取る
  const flatJson = jobj.map((e) => testStatus(e));
//配列に格納する
  const jsonResults = Object.entries(flatJson);
  const testResults = Object.values(flatJson);

//JSONファイルの結果は全て記録対象
  var suffix = 0;
  jsonResults.forEach(function(jsonResults){
    //テスト設定名
    var workflowName = Object.values(jsonResults)[1]["workflowName"];
    Logger.log(workflowName);
    //テスト実施日
//JSTに再計算
    var createdAtUTC = Object.values(jsonResults)[1]["createdAt"];
    var createdAt = calcJst(createdAtUTC).slice(0, 10);
    Logger.log(createdAt);
    //テスト実施時刻、テスト計画から取得する
    var timedata = searchTestCase(createdAt, workflowName);
    Logger.log(timedata);
    //テスト結果
    var status = Object.values(jsonResults)[1]["status"];
    Logger.log(status);
    retArray.push([workflowName, createdAt, timedata, status]);
    suffix = suffix + 1;
  });
//  }
  return retArray;
}

```
testStatusという関数は、JSONデータの中から記録に必要な「テスト名」と「実施日」と「テスト結果」を抜き出す関数で、
```
//magicpod-analyzerから取得したJSONをテスト名と実施日とテスト結果だけにする
const testStatus = (obj) => {
  const result = {};
  for (const key in obj){
    const value = obj[key];
    if (typeof value === "object"){

    }else if ((key == "createdAt") || (key == "workflowName") || (key == "status")){
      result[key] = value;
    }
  }
  return result;
};
```
前述したUTCをJSTに読み替える関数 calcJst は、
```
function calcJst(date){
  if (date === '' ) {
    return ''
  } else {
    const d = new Date(date);
    var format = 'yyyy-MM-dd HH:mm:ss';
    var retval = Utilities.formatDate(d, 'JST', format);
    return retval;
  }
}
```
読み替えた時刻から年月日部分のみsliceしてテスト開始日付としています。
テスト開始時刻は、あくまでもテスト計画に記載の時刻を記録に用いるため、関数 searchTestCase でテスト計画から実施時刻を抽出しています。
```
//テスト計画シートから、実行時刻を取得
function searchTestCase(createdAt, workflowName){
  //名前付き範囲を作成しておき、テスト結果の曜日から範囲を選んで使う。
　var arr_day = new Array('rangeSunday', 'rangeMonday', 'rangeTuesday', 'rangeWednesday', 'rangeThursday', 'rangeFriday', 'rangeSaturday');

　var bk = SpreadsheetApp.getActiveSpreadsheet();
　var sh = bk.getSheetByName("テスト計画");
　//var rng = sh.getActiveCell();

　var day_num = new Date(createdAt).getDay();
  var rangeName = arr_day[day_num];
  Logger.log(rangeName);
//選んだ範囲を配列に格納する
　var rngArray = bk.getRangeByName(rangeName).getValues();
  var timeData = getTestTime(sh, rngArray, workflowName);
  return timeData;
}
```
ここで、テスト計画シートに設定しておいた、名前付きセル範囲を使います。テスト実施の曜日別に設定しておいたセル範囲から、関数 getTestTime でworkflowNameをキーにして実施時刻を調べています。
GAS初心者としては、検索機能を実装するのに適切なやり方を知りません。いろいろ巷の参考資料を見ながら、一旦、配列に格納してから検索するのが常套手段のように思えましたので、そのようにしています。
```
function getTestTime(sh, rngArray, workflowName){
  var pattern = '/' + workflowName + '/';
    
  for(var i = 0; i < rngArray.length; i++) 
  {
    if(rngArray[i][0].indexOf(workflowName) !== -1)
    {
      var timeRow = i + 1;
      var timeDataCell = 'A' + timeRow;
      var timeData = sh.getRange(timeDataCell).getValue(); 
    }
  }
  return timeData;
}
```
テスト結果（SUCCESS / FAIL）は、
```
var status = Object.values(jsonResults)[1]["status"];
```
でJSONデータから読み出しています。
ここまでで、JSONデータから、「テスト名」「テスト実施日」「テスト実施時刻」「テスト結果」が
配列 resultArray　に格納されましたので、次は、テスト結果表に書き出します。
mainスクリプトから呼ばれる recResult は、
```
function recResult(resultArray){
  var bk = SpreadsheetApp.getActiveSpreadsheet();
  let lastRow = bk.getLastRow();
  let lastCol = 49;
　var sh = bk.getSheetByName("テスト結果").activate();
  var testDay = sh.getRange(1, 1, lastRow).getValues();
  
  for(var suffix = 0; suffix < resultArray.length; suffix++){
  //A列からテスト実施日を探す
    for(var tD = 0; tD < testDay.length; tD++){
      if(testDay[tD][0].indexOf(resultArray[suffix][1]) !== -1){
        var recRow = tD + 1;
      }
    }
    //1行目からテスト実施時刻を探す
    for(var col = 1; col <= lastCol; col++){
      if(sh.getRange(1, col).getValue() === resultArray[suffix][2]){
        var recCol = col;
      }
    }
    var testStatus = resultArray[suffix][3];
    if(testStatus == 'SUCCESS'){
      sh.getRange(recRow, recCol).activate();
      sh.getActiveRangeList().setFontColor('BACKGROUND');
      sh.getRange(recRow, recCol).setValue('success');
      sh.getActiveRangeList().setBackground('#0000ff');  
    }
    if(testStatus == 'FAILURE'){
      sh.getRange(recRow, recCol).activate();
      sh.getActiveRangeList().setFontColor('#000000');
      sh.getRange(recRow, recCol).setValue('failed');
      sh.getActiveRangeList().setBackground('#ff0000');  
    }
  } 

}
```
テスト結果シートの縦軸（A列）は日付、横軸（1行目）は時刻の表ですので、配列に格納された「テスト実施日」「テスト開始時刻」からテスト結果シートのA列と1行目を検索して、
該当したセルに「テスト結果」を記入しています。
# 総括
自動テストの定期実行結果を表にまとめるところまでGASを使って半自動化できました。
GASも初めてのわりには、お見苦しい点がありますが、なんとかなりました。
MagicPodには、テスト一括実行結果のリスト画面もあり、また、メールやSlackによる通知機能もあるので利用していますが、原因不明のテスト失敗が起こった場合、開発中のdev環境だけなのか、それとも、お客様とも共有しているstg環境にも及ぶのかによって、事の重要性は違うでしょう。
dev環境なら、開発者が先行して修正版をリリースしていることもあるかもしれません。自動テストも積極的に先行して準備できる機会と考えれば、失敗テストも大切な情報源と思えます。
ただし、そんな風に思えるのは、ある程度の心のゆとりがないと実際には困難で、
自動テストは、何等の異常が無ければ日々のテストを粛々と愚直に実行してくれてとても助かりますが、異常発生時には、
* ログを見たり
* スクショを見たり
* 手動テストでトレースしてみたり
と、世話が焼ける面も実際にはあります。レポート報告やテスト結果記録について、できるだけ手のかからない手段を講じておき、特に多くのテストを自動テストで実施している場合は、リストや通知にテスト失敗の事実が埋もれてしまわないような、異常に気がつく、見逃さない工夫をしていきたいものです。
# 参考にした資料
magicpod-analyzerやGASプログラミングについて、参考にしたサイトを以下に書き出しました。
なんとかなりますヨー。やってみましょう。
* [GAS入門講座](https://note.com/skipla/n/n0803937a0887)
* [GAS関数作成と呼び出し方法](https://note.com/youtube_consul/n/n1ddacb34847a)
* [APIで取得したJSONをスプレッドシートに展開したい](https://codelife.cafe/entry/GAS-JSON-to-SpreadSheet)
* [GASでタイムゾーンを日本時間に変更する方法](https://hoshino-wp.com/gas-timezone/)
* [MagicPod導入事例](https://magic-pod.com/customer-stories/)