テストデータの準備を円滑化しよう

冨田

テストデータの準備を円滑化しようの目次

  • データ投入の課題
  • プロセスの制度化
  • Soymilkの開発
  • 効果

こんにちは。CTOの冨田です。 前回のブログ記事ではシステムからデータを円滑に取得するためにGraphQLを活用した取り組みを紹介しました。 今回は反対側、データを準備するプロセスについて紹介します。

データ投入の課題

今回のテーマであるテストデータとは本番同等構成でアプリケーションを検証するときに必要なものを指します。 このような環境はステージング環境、開発環境、試験環境などと呼ばれます。 当ブログでは社内の呼び方にあわせて開発環境(dev環境)とします。

開発環境は本番同等構成なので原則スマートフォンアプリやウェブサイトの操作をおこなってデータを準備するルールにしています。 しかし遠い過去のデータが欲しい、大量のデータが欲しい、SMS番号など外部リソースに依存する部分を迂回したいなど、ルールどおりにできないケースがあります。 このため開発環境にはデータ投入用のスクリプトを用意することが多いです。

データ投入用のスクリプトはSQLなどデータ永続化層の操作言語か、アプリケーションの実装言語で書くのが一般的です。 kippでも一番最初は開発者が開発環境のみMySQLに直接ログインできるよう構成し、手書きのSQLを実行していました。 この方法は属人的で誤操作のリスクが高いのみならず、行暗号化されるテーブルのデータを用意できない欠点がありました。 そこでScalaスクリプトを受け取りサーバ上で実行する仕組みを用意しました。 開発者はデータを用意したり更新するスクリプトを準備し、リモート実行コマンドを呼び出すことでサーバ上のデータを自由に改変できます。 スクリプトをテスト、レビュー、共有することで最低限のエコシステムが出来ました。 しかしスクリプトはアプリケーションコードと同じコードベースを使って実行するので、十分な知識が必要です。 スクリプトがIDEを使ってかけない点も開発体験が悪く、あまり使われませんでした。

あらためてすべてのチームに聞き取りを行いニーズと問題点を整理しました。

  • データ更新の要望は少なく、ほとんどが追加である。追加だけできれば90%以上の要求に応えられる
  • パートナーによるテスト期間内にあたらしいデータが必要になることも多く、すばやく対応してほしい
  • Scalaスクリプトは書きにくくSQLで無理やり作業している。SQLで対応できないものは発生しないよう回避している。結果パートナーとの軋轢が生まれる
  • パートナーから渡される要望が曖昧で開発者には理解できない。意思疎通のための時間がかかる
  • テスト期間はこまかい単位で何度も依頼が来て開発者の集中が妨げられる

プロセスの制度化

以上の課題を解決するため、まず投入のプロセスを制度化しました。 ルールとマニュアルを作成すれば属人性を排除でき、自動化も容易になります。 投入で重要なのはデータフォーマットです。 自然言語でニーズを表明されると、意味を解釈しデータを作成するという二段階が余計に挟まります。 時間がかかりミスも起きやすいです。 そこで最初から作成したいデータの全体を指定し、テキストファイルで受け渡すようにしました。

データといっても、ほとんどただのScalaファイルです。 次のような構造体を想定します。

type UserId = String
type CardId = String

final case class User(
  // Userの識別子
  userId: UserId,
  // 登録日時
  createdMs: Long,
  // 暗号化済み個人情報
  encryptedPersonalData: EncryptedMessage[PersonalData],
)

// 個人情報、要行単位暗号化
final case class PersonalData(
  // Userの氏名
  name: String
)

このとき投入データは次のようになります。 操作したいテーブルとファイル名は対応づけ、user.txtとするルールです。

User(userId = "XXX", createdMs = 1626663292000, personalData = PersonalData(name = "Yamada"))
User(userId = "YYY", createdMs = 1626663292000, personalData = PersonalData(name = "Sato"))

ご覧のとおりただのScalaファイルです。 EncryptedMessageなど内部的な仕組みは隠蔽し、GraphQLの構造に近づけています。 この形式ならGraphQLやサンプルファイルを見ながらデータ作成できるだろうと算段しました。 大量に連番のデータを作りたいときは表計算ソフトでデータテーブルを作成し文字列結合してテキストを準備できます。 開発者は具体的なデータで受け取るので、コピペしてすこし整形するだけでスクリプト化できます。

Soymilkの開発

フォーマットを決めたことで開発者の作業は簡単になり、自動化もできそうに思えました。 書き換えやAWSコマンドの実行を自動化し開発環境を不要にすればパートナーとコミュニケーションしている支援チームが作業できます。 この自動化をScalaで実装し、fat-jar形式で社内に配布しました。 社内で愛されるように、投入と豆乳をかけてSoymilkと名前をつけ、アイコンも作りました。

soymilk icon

Soymilkを利用すると次の1コマンドでデータフォーマットの確認、スクリプトの生成、AWS ECS上での実行が完了します。

java -jar soymilk.jar user.txt

Soymilkはデータを受け取りサーバに準備されたコマンドが解釈できるスクリプトに変換する部分、AWS ECS APIを呼び出す部分の2つで構成されます。 前者はScalametaを利用します。 上の例のように、入力ファイルは1行ずつ見れば文法レベルで正しいScala式ですが、型は合っていません。 また一行ずつはただの式で、それをまとめる構造がないのでファイル全体が正しいScalaファイルではありません。 便宜的にobjectで包んだものを構文解析(parse)し、型を利用せず名前にもとづいたパターンマッチで整形します。

s"object X {$lines}".parse[Source] match {
  case src: Parsed.Success[Source] =>
    src
      .get
      .collect { case q"object X { ..$stats }" =>
        val inserts = stats.zipWithIndex.collect { case (s: Term, i) =>
          val msg = s"Insert $fileName $i-th record"
          val adjusted = wrapEncryptedFields(s)
          Seq(
            q"println($msg)",
            q"Await.result(db.run(tables.${Term.Name(fileName)} += $adjusted), Duration.Inf)",
          )
        }
        inserts.flatten
      }
      .flatten
  case err: Parsed.Error =>
    error(s"Cannot parse $path, Check file format is correct. $err")
}

ECS APIの呼び出しはAWS SDK for Javaを用います。 ECS Taskを実行するには様々なパラメータを指定しますが、すでに動いているタスクから取得するなどして隠蔽しています。 aws-cliのインストールなどを排し、Java実行環境だけあれば作業できるようにしました。 AWS credentialsのセットアップやコマンドの利用方法は社内ドキュメントに事例つきでまとめています。

効果

フォーマットの策定は効果的で、当初の課題はすべて解決できました。 開発者は一切作業しなくてよくなり、新規開発に集中できます。 要求へのレスポンスが大幅に改善し、投入できるかどうかの不自然な制約がなくなり、パートナーとのやりとりが円滑になりました。 たしかに投入したいデータを作成する人の負担は増えていますが、データの決定はテスト設計でなされるので元来必要なものを整形するのみです。 頻繁に利用されるテーブルにはSpreadsheetのテンプレートを用意し穴埋めで投入データが作れる仕組みも用意しました。 ミスや不整合があるときもこの行のこの部分と具体的に会話できます。

より高い視点では技術の民主化を推し進められた成果がありました。 データを書き下すのは煩雑にも見えますが、支援チームのデータ構造への理解が深まる効果をもたらしました。 だれでも、いつでも作業できるようにドキュメントを整備する意識も向上しました。 これらの効果はデータ投入だけでなく他の業務も効率化し、短時間で楽しく作業できるようになりました。

今後改善すべき点はテーブルをまたいだ整合性の問題です。 たとえば取引を通知するには取引テーブル、通知全体のテーブル、通知と通知先(メール、プッシュなど)を関連づけるテーブルの3つにデータが必要です。 サービスを企画する視点では取引テーブルに注意が集中し、他のテーブルを忘れがちです。 一方で大量に取引を追加するためにいちいち通知を送信したくない場合もあります。 さらにこのような複数のテーブルで一連の事象を表すケースは多数あり、ひとつずつルール化するには相当な手間がかかります。 現在はファイルを受け取った時に意思確認をしていますが、ゆくゆくは徐々にプログラムでカバーしたいです。

Kippでは他にも業務効率化、コミュニケーション効率化に取り組んでいます。 ルールの策定とScalaやRustを利用した自動化で楽しく円滑に働ける環境を目指しています。 そんな環境で働いてみたい、環境をつくってみたいという開発者の方はお気軽にpeople@kipp-corp.comまでご連絡ください。