Slide 1

Slide 1 text

2024-07-25 NEWT Tech Talk vol.10 Shumpei IINUMA ( 株式会社令和トラベル) NEWT Backend (GraphQL) の自動テストの現状とこれから ~ 2 年間の開発・運用で遭遇した課題と工夫、今後の課題 ~

Slide 2

Slide 2 text

Shumpei IINUMA 令和トラベルでバックエンドエンジニアをしています 新しい旅行を体現するアプリ「NEWT 」と、それを提供 するための旅行管理システムのAPI やインフラの開発 ~ 2015 大学院で自然言語処理を学ぶ 2016 ~ 2017 リクルートでサイト内検索ロジックの開発 2017 ~ 2021 SaaS プロダクトの開発 2022 ~ 令和トラベルにジョイン 主な仕事内容 略歴 @iinm_stderr @iinm

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

前提:バックエンドチームの主な活動領域 Public Endpoint Private Endpoint Admin NEWT Android Team iOS Team Frontend Team Backend Team API 社内向け ツアー企画・手配業務 カスタマー向け 海外ツアー・ホテル予約 今日話す「自動テスト」の対象

Slide 5

Slide 5 text

API のテストとは何をテストしているのか? ( 各所でテストしてますが...)

Slide 6

Slide 6 text

例:バリデーション目的の違い UI 制御のバリデーション カスタマー体験を良くするために 即時フィードバックするため データの整合性は担保しない API のバリデーション データの整合性を担保 仮にUI に不具合があっても不正な データが作られないよう制御 UI からは入力し得ないようなケースも含め システムの整合性を守る

Slide 7

Slide 7 text

テストサイズによる分類 Large ( それ以上の環境) Medium ( 単一マシンで実行可能) Small ( 単一プロセスで実行可能) Large :開発環境にデプロイしてテスト UI から操作するテスト 負荷テスト Medium :Docker 上のDatabase を利用したテスト Small :単一プロセスでのテスト GraphQL エンドポイントのテスト ※GraphQL 以外のHTTP エンドポイントもある 外部システムはテストダブル エンドポイント単位ではパターン網羅が大変 なので処理を切り出した部分のテスト 「ツアーの料金計算」など、プロセス外に副作用 のないロジックのみのテスト API の自動テスト 実行コスト

Slide 8

Slide 8 text

テストサイズによる分類:API 自動テストのサイズごとの比率 Large N/A Medium 718 files Small 146 files GraphQL エンドポイントのテスト (60%) それ以外のテスト (40%) 大部分が、エンドポイント単位でのテスト または データベースアクセスとの通しでのテスト 特にパフォーマンスが重要な機能の改修時 シナリオを書いて負荷テストをしている File の単位 Medium: エンドポイント単位 Small: 関数 テストケース数:合計5,450 ケース

Slide 9

Slide 9 text

Medium が多くなった経緯:意識してそうなったわけではない 😅 約3 年前、最適な状態が見えてきたら整理しましょう、と内部構成を整理 せずに勢いよく作り始めた。例えば、GraphQL レイヤーからデータベース アクセスするなどの無秩序な状態からのスタート。 ⇒ エンドポイント単位でテストを書く以外に選択肢がなかった。 とはいえ、在庫料金の計算などクリティカルな機能部分は純粋関数に切り 出してSmall テストで高速に様々なパターンをテスト可能にしている。

Slide 10

Slide 10 text

2 年間の開発・運用で遭遇した課題・工夫ポイント

Slide 11

Slide 11 text

メンテナンス性 テストケース継続的に育てられるか?

Slide 12

Slide 12 text

課題:テストが全体的に読みづらい 🤔 テストコードをレビューするタイミング タイトルでテスト観点を明示するところまでは当然やったうえでの話 前提条件となるデータのセットアップ、実行、検証が煩雑で、どこがポイントなのかが 分からない。 工夫: Given-​ When-​ Then 記法:セットアップ、実行、検証のフェーズを明記 開発メンバーの中にSpock Framework の利用経験者がいて 提案してくれた。 コメントではなく、記載を強制で きるライブラリもある。

Slide 13

Slide 13 text

課題:期待値の視認性が低いケース 👀 例:ツアーの旅程表の生成結果 (数百行続く) 出力が巨大なJSON のパターン 期待値のサイズが大きすぎて、正しい 結果であることを確認することが難し い。 期待値を直接書くのではなく、Jest の Snapshot 機能を使って書き出された結 果を目視でチェックしていた。 ある日、チェックしきれずに不具合を 含んだまま本番にリリースされること に 😱

Slide 14

Slide 14 text

課題:期待値の視認性が低いケース 👀 例:旅程表の生成結果 工夫:人間が読みやすい形式 人が読める形式に変換後にSnapshot を記録。 テストケースによっては、確認したい特定の 日付の旅程のみに絞る。

Slide 15

Slide 15 text

課題:期待値の視認性が低いケース 👀 活用例:ツアー料金の計算結果の確認 右の例では旅行代金だけ切り出している が、実際には諸税等、他にも多くの要素 が含まれるため、確認が大変。 カスタマーが目にする最終結果だけでな く、計算過程をグラフで表現、人が読め るテキスト形式に変換してSnapshot を確 認。 ※ formatResult の第2 引数 (2) は計算グラフの深さ

Slide 16

Slide 16 text

信頼性 結果が信頼できるテストか?

Slide 17

Slide 17 text

API 自動テストによるコードカバレッジ (=品質ではないが、参考値として) 緑 (>=80%) 、黄色 (>=50%) 、赤 (<50%) ポイント、クーポンなど、不具合が致命 傷になるモジュールは90% 超えていてい るものが多い。 モジュール単位のカバレッジ

Slide 18

Slide 18 text

課題:有効なテストデータになっているか? 例えば ツアーの予約をテストするには、ツアーが必要。 ツアーを作るには、フライト、ホテルなど様々な依存データの設定が必要。 毎度セットアップするのは大変なので、データセット作成用の関数を用意している。 何が問題か? データセット作成用の関数がDatabase に直接insert していた場合、バリデーションを通してない ので、本番環境にはないような不正なデータでテストしている可能性がある。 例えば、特定日の在庫を0 に書き換えるなど、データセットの一部設定を書き換えてテストに利 用することがある。この際、自分が作ってない(詳しくない)モジュールのテーブルを直接書き 換えることがある。詳細を調べるのに時間が掛かるうえ、使い方を誤る危険性がある。(実際に これ起因で致命的不具合が生じたことはまだないが)

Slide 19

Slide 19 text

課題:有効なテストデータになっているか? 工夫(In Progress ) テストデータ作成にはできる限り本番環境と同じ方法を利用する。GraphQL のエンドポイントまた は、そこから呼び出されるデータ作成・更新関数を利用して整合性の取れたデータセットを作る。

Slide 20

Slide 20 text

その他、やりたいけどできていないこと 開発環境 / 本番環境にデプロイした状態での自動テスト 現状:デプロイ後に画面を通して、ツアー検索から予約までの主要動線を動作確認 理想:デプロイ後、主要動線が動くことを確認、その後デプロイしたバージョンに切り替え

Slide 21

Slide 21 text

パフォーマンス 開発のボトルネックにならないか?

Slide 22

Slide 22 text

良かったこと:テストが実装の詳細に依存しすぎない (意図してそのように設計されたわけではなかったが😅)結果として 基本的にGraphQL エンドポイントレベルでのテストをしている。 原則、コントロール不能な外部システム以外はテストダブルを使って ないため、内部構造をリファクタしやすい。 (過去の反省)以前別プロジェクトで、テストピラミッドを極端に実践して、データベースアクセスも依存 クラスもすべてテストダブルを利用してテストを書いていたことがあった。テストの実行速度は速かったが 、あるクラスの振る舞いを変えたときに、依存するクラスのテストが壊れ、修正箇所が多くなる傾向があっ た。 エンドポイントレベルからテストを始めて、大量のパターン網羅が 必要な箇所を切り出してSmall サイズテストで検証可能に

Slide 23

Slide 23 text

課題:データベースに依存したMedium テストが遅い、並列実行できない ほとんどのテストがデータベースとやり取りするため、Small テストと 比べると遅い。 ( 初期の設計ミス) テスト実行前にすべてのテーブルをtruncate する前提 → いつの間にかテストの期待値としてデータベースで発番されるauto increment のID が混在 → truncate やめられないし、並列実行できない \(^o^) / 理由

Slide 24

Slide 24 text

Try :テストを並列実行可能に書き換える(辞めました) テストの期待値がauto increment のID に依存しないよう書き換え 作ったデータをテスト終了時に消す(ユニーク制約違反が発生することがあるため) 方法 並列実行することで、ロックによるリソース競合の問題も検知できるのでは? その他期待した効果 後述の方法で並列実行できるようになった リソースの競合のテストは、すでに書いているし、テストすべき観点なら明示的に書くべき 辞めた理由

Slide 25

Slide 25 text

テストの並列実行とデータクリーンアップの高速化 Test worker process ごとにDatabase を用意することでクリーンアップタイミングを 気にせず並列実行可能に。 ※ テストを実行するマシンの台数を増やす方法もあるが、マシン ごとのセットアップ処理の重複の無駄が無視できないレベルになる。 テスト中のデータベースアクセスを記録して、利用したテーブルのみ自動でtruncate するこ とでデータクリーンアップを高速化。 方法 Machine #1 Database process on Docker Test worker #1 Test worker #2 Test worker #3 Test worker #4 Test database #1 Test database #1 Test database #1 Test database #1 x N 台 参考 テストファイルの総数は864 件 (5,450 件のテストケース) GitHub Actions 上の 8 core / 32 GB Memory のマシン4 台で15 分 で実行可能

Slide 26

Slide 26 text

実行速度に関する今後の課題 リリース前は全テスト実行、PullRequest に対しては変更があったモジュー ルのみをテストしている。 リリース前のテスト:影響のあるモジュールのみを対象にしたい。 PullRequest に対するテスト:変更があったファイルを含むモジュール のみを対象としているため、その後の全テスト実行で落ちることがあ る。依存関係まで考慮して対象を絞れるようにしたい。 テスト実行スコープの制御

Slide 27

Slide 27 text

サマリー:NEWT Backend (GraphQL) の自動テスト 大部分をMedium サイズのGraphQL エンドポイントレベルでテスト 最低限のテストダブルで実装の詳細に依存しすぎないテスト 方法 Given-​ When-​ Then 記法の活用 人が読みやすいSnapshot テスト マルチコアを有効活用できる実行環境 工夫ポイント 効率良く有効なテストデータをセットアップする仕組み 開発環境 / 本番環境に対する自動テスト Medium サイズのテストの実行速度 / 実行スコープの制御 (あるいはシステム分割) 今後課題