Unityエンジニアの尾崎です。
弊社のサービスで構築しているマスターデータの自動チェックツールについて紹介します。
ツール作成の背景、実装上のポイントについてもお伝えします。
はじめに
サービスの開発中QA*1担当者がテスト項目書を元にサービスの動作テストを行います。
テスト項目数が多くなってくると手動でのテストは時間がかかるものになります。
そこでテストを自動化するツールを構築しています。
ツールについて
スマホゲームのサービスでよく行われるイベントの開発を例にツールの仕組みを解説します。
まずイベントの開発は以下のような流れで行っています。
イベント開発の流れ
- 設計書を作る
- マスターデータ*2を作成
- テスト項目書を作成
- テスト項目書に沿ってサービスをテスト
- リリース
まずはイベントの設計書を作ります。この設計書はGoogleスプレッドシート*3で作っています。
その設計書を元にマスターデータを作成し、サービスに反映します。こちらもスプレッドシートで作成しています。
次にテスト項目書を作成します。設計書を元にテストすべき項目をリスト化します。
そして項目書に沿ってイベントのマスターデータが反映されたサービスでテストを行い、不具合がなければリリースという流れになります。
設計書とマスターデータは以下のようなものです。
設計書 | マスターデータ |
---|---|
※ 内容は実際のものとは異なります
サービスのプログラム修正が伴わないイベント開発の場合、不具合の原因は設計書通りにマスターデータを作成できていないことがほとんどです。
そこで設計書通りにマスターデータが設定されているか自動でチェックするのが今回紹介するツールです。
仕組み
設計書の値とマスターデータの値を照らし合わせて正しく設定されているか自動でチェックします。
設計書もマスターデータもどちらもGoogleスプレッドシートで作成されているので、Google Apps Script*4で構築しています。
チェックツール自体をスプレッドシートで作成していて、チェック結果はシート上にログとして書き出す仕組みです。
なおサービスのプログラムに変更があった場合にはマスターデータ設定以外の原因で不具合が起きる可能性があるので手動チェックも行います。
画面
チェックツール用のスプレッドシートのカスタムメニューから実行を開始します。
実行画面
イベントの実施年月を入力してチェックを開始します。
結果画面
チェック結果用のスプレッドシートにログの形で出力しています。
実装について
ツールの実装を紹介します。
処理の流れを分かりやすくするためエラー処理などは省いています。
チェックスクリプト
設計データとマスターデータを読み込み、チェックを行ってログに書き込むという流れになっています。
// スプレッドシートのUIを開いてイベント実施年月を入力 let ui = SpreadsheetApp.getUi(); let result = ui.prompt('イベントの年月を入力してください。', ui.ButtonSet.OK_CANCEL); let selectedButton = result.getSelectedButton(); // 入力キャンセル if (selectedButton == ui.Button.CANCEL || selectedButton == ui.Button.CLOSE) return; let yearMonth = result.getResponseText(); // 結果をシートに出力するログ用オブジェクトを作成 let logger = new SheetLogger(SpreadsheetApp.getActiveSpreadsheet().getId(), `ログ`); // 状況をログシートに出力 logger.info(`イベント設計データがマスターデータに正しく設定されているかチェックします。`); // イベント設計データを読み込み logger.log(`設計データをロードします。`); let planDataSet = new EventPlanDataSet(yearMonth); planDataSet.load(); // マスターデータを読み込み logger.log(`マスターデータをロードします。`); let masterDataSet = new MasterDataSet(); masterDataSet.load('event_stage_item'); let ok = 0; let ng = 0; // 設計データのステージごとにチェック for (let stageItem of planDataSet.stageItems) { for (let item of stageItem.items) { logger.infoToBuffer(`チェック: event_id: ${planDataSet.eventId}, stage_id: ${stageItem.stageId}, items: ${stageItem.items}`); // event_stage_itemマスタにアイテムが存在するかチェック let exists = masterDataSet.eventStageItem.exists(planDataSet.eventId, stageItem.stageId, item); // マスタに正しく設定されていれOK、なければNG if (exists) { logger.fineToBuffer(`OK: ${item}`); ok++; } else { logger.errorToBuffer(`NG: ${item} (event_stage_itemマスタにevent_id:${planDataSet.eventId}, stage_id:${stageItem.stageId}, item:${item}が設定されていません。)`); ng++; } } } // チェック完了メッセージをログとスプレッドシートUIに表示 let message = `チェックが完了しました。\nOK: ${ok}, NG: ${ng}`; logger.info(message); SpreadsheetApp.getUi().alert(message);
データ読み込み
上記チェック用スクリプトに出てきたイベント設計データを扱うEventPlanDataSetクラスです。
設計書のスプレッドシートからデータを読み込みます。
/** * イベント設計データセット * 設計データのシートを扱う */ class EventPlanDataSet { /** * コンストラクタ * @param {string} yearMonth 年月を表す文字列 (例. 2110) */ constructor(yearMonth) { this.yearMonth = yearMonth; /** * イベントID */ this.eventId = 0; /** * ステージ報酬アイテム */ this.stageItems = []; } /** * スプレッドシートからデータをロードする */ load() { let fileName = `イベント_${this.yearMonth}`; let files = DriveApp.getFilesByName(fileName); let spreadsheet = SpreadsheetApp.open(files.next()); // シートを取得 let sheet = spreadsheet.getSheetByName('イベント設計'); // イベントIDを取得 // データ位置が移動しても影響しないようにできるだけセル位置での指定を避けデータタイトル名から検索する let eventIdTitle = sheet.createTextFinder('イベントID').findNext(); let eventId = eventIdTitle.offset(2, 0).getValue(); // 報酬アイテムのタイトルセルを取得 let itemTitle = sheet.createTextFinder('報酬').findNext(); // 報酬データ開始セル let firstItemRange = itemTitle.offset(2, 0); // セルデータ取得 let values = sheet.getRange( firstItemRange.getRow(), firstItemRange.getColumn(), sheet.getLastRow() - (firstItemRange.getRow() - 1), 3 ).getValues(); // セルデータをクラス化 this.stageItems = values .map((x, index) => new EventStageItemPlanData( index + 1, x )); this.eventId = eventId; } } /** * イベントの報酬アイテムデータ * 設計データのシートの一行を扱う */ class EventStageItemPlanData { /** * @param {number} stageId ステージID * @param {array} items 報酬アイテムの配列 */ constructor(stageId, items) { this.stageId = stageId; this.items = items; } }
工夫しているところ
モダンJavaScript活用
2020年3月にJavaScriptランタイムとしてV8が導入されました。
従来のランタイムはRhinoでJavaScriptのバージョンは(ES5)(ECMAScript 2009)でした。
V8ではES6(ECMAScript 2015)に対応し、クラスやアロー関数などモダンな機能が利用できるようになりました。
それらを活用することで従来よりきれいなコードを書くことができます。
クラス
Apps Scriptは従来関数のみで構成することが多かったですが、V8ランタイムではクラスが使えるようになりました。
オブジェクト指向プログラミングでシステムを構成することができます。
このツールではシート、行などをそれぞれ1つのクラスで表現しています。
スプレッドシートからのデータロードの処理はシート上の位置をハードコーディングしたり煩雑になりがちですが、クラス内にカプセル化することでチェックを行うスクリプトからスプレッドシートの処理を分離することができ、すっきりとしたコードになっていると思います。
またクラスにはメソッドやアクセサを定義することができます。複雑な検索処理などはメソッド化してチェックスクリプトを簡潔に済ませるようにしています。
以下は、マスターデータシートを扱うクラスにデータ検索メソッドを定義しているコードです。
/** * イベント報酬アイテムのマスタデータリスト * * イベント報酬アイテムマスタシートを扱う */ class EventStageItemMasterDataList extends Array { /** * コンストラクタ * マスターデータ全体を扱うクラスからシートのデータが渡されます * @param {object[]} シートからロードしたデータ */ constructor(values) { super(); values // データクラスに変換 .map(data => new EventStageItemMasterData(data.eventId, data.stageId, data.item)) .forEach((x) => this.push(x)); } /** * イベントステージアイテムが存在するか * @param {number} イベントID * @param {number} ステージID * @param {string} アイテム名 */ exists(eventId, stageId, item) { return this.some( x => x.eventId == eventId && x.stageId == stageId && x.item == item ) } /** * イベントIDのデータを取得する * @param {number} イベントID */ getByEventId(eventId) { return this.filter(x => x.eventId == eventId); } }
キャッシュ
スプレッドシートのデータが大きくなるとデータロードにかなり時間がかかるようになってきます。1万件以上のデータが入力されたスプレッドシートを扱うこともあります。そのため一度データロードしたものはキャッシュに保存しておき2回目以降はキャッシュからロードすることでロード時間を短縮しています。
キャッシュにはApps ScriptのCacheServiceを使っています。以下のようなコードで利用できます。
let key = 'test'; let value = 'キャッシュデータ'; let cache = CacheService.getScriptCache(); // キャッシュに保存 cache.put(key, value); // キャッシュから取得 let cachedValue = cache.get(key); console.log(cachedValue); // 'キャッシュデータ'
しかしCacheServiceには1つのキーごとのキャッシュデータが100KBまでという制限があります。そこで100KB以上のデータを100KBの複数データに分割して保存しています。
その際のコードを紹介します。
まずはキャッシュに保存するときのコードです。
// 100KB以上の大きなデータ let data = '100KB以上の大きなデータ'; // キャッシュキー let key = 'some_cache_key'; // json化 let json = JSON.stringify(data); // jsonのサイズを取得 let size = Utilities.newBlob(json).getBytes().length; // キャッシュにまとめて保存するオブジェクト (key: 分割用キャッシュキー, value: キャッシュデータ) let values = {}; // 100KBで分割 const splitSize = 100000; // 分割数 let splitCount = Math.floor(size / splitSize); // jsonを分割 for (let i = 0; i <= splitCount; i++) { // キャッシュ1件の上限である100KBごとにjson文字列を分割する let splitedJson = json.slice(i * splitSize, splitSize * (i + 1)); values[`${key}_${i}`] = splitedJson; } // キャッシュに保存 CacheService.getScriptCache().putAll(values); // プロパティサービスにキャッシュ分割数を保存 PropertiesService.getScriptProperties().setProperty(`cache_split_count_${key}`, splitCount.toString());
次にキャッシュから取り出すときのコードです。
// 分割数をプロパティーから取得 let splitCount = parseInt(PropertiesService.getScriptProperties().getProperty(`cache_split_count_${key}`)); // キャッシュキーの配列 let cacheKeys = []; for (let i = 0; i <= splitCount; i++) { cacheKeys.push(`${key}_${i}`); } // 分割されたデータをまとめて取得 let values = CacheService.getScriptCache().getAll(cacheKeys); // 1つのjsonに連結 let json = ''; for (let key of cacheKeys) { json += values[key]; } // 分割してキャッシュに保存する前のデータ let data = JSON.parse(json);
まとめ
設計書とマスタデータの自動チェックツールについて紹介しました。
自動チェックの対象は特に手動チェックだと時間がかかるところから始め、範囲を広げていっているところです。
このツールによって手動チェックの時間を1割ほど減らせました。
また、Apps Scriptの工夫ポイントについてもお伝えしました。こちらはツールの内容によらず活用できる内容だと思いますので、何かの参考になれば幸いです。
*1:「Quality Assurance・品質保証」を意味しサービスの動作テストを行うことです
*2:サービスを動作させるために事前に必要なデータです。サービス動作後に作られるデータはユーザーデータと言い、マスターデータと区別しています
*3:Googleのオンライン表計算ソフト www.google.com
*4:Googleのサービスを自動化するためのスクリプト workspace.google.co.jp