『呪術廻戦 ファントムパレード』のバックエンドにおけるテスト実装の取り組み

この記事は、2024年3月7日に開催された「CyberAgent Game Conference 2024(CAGC 2024)」のセッション内容をAIによる自動文字起こしをベースに加筆修正したものになります。

セッション概要

『呪術廻戦 ファントムパレード』のバックエンドチームでは、大規模なソース開発を安定して行うためテスト実装についてルールを策定し取り組んでいます。
本セッションでは、ファンパレのバックエンドにおけるテスト実装の取り組みについてご紹介します。
具体的にどのようなテストを実装しているのか、どのようなことを意識してチーム内で取り組んでいるのかをご説明しつつ、テストを活用した仕組みの例もご紹介させていただきます。

※セッションのアーカイブ動画

登壇内容

テスト実装について

バックエンドにおけるテスト実装の役割は、テストを通してAPIや実装の振る舞いが正しい事を動作確認することにあります。

我々バックエンドのチームの主な役割は、クライアントからのリクエストを処理するAPIを作成することになります。
リクエストというと例えば、「ガチャを引きます」、「メインクエストに挑戦します」、「強化してレベルアップします」等です。
そういったリクエストが、クライアント、つまりスマートフォンから送られてきます。
そのデータをバックエンドチームの作成したAPIが受けて正しく処理する必要があります。

その作ったAPIは本当に正しく動作するのか?
というのを確かめるため、仮のデータをAPIに流してみる事で正しく動作するかを確かめる。これがテストの主な役割になります。

図の例だと、レベルアップするためのリクエストを仮で作ってみて、APIで実行する。その結果が、ちゃんとレベルアップしたデータになってるか?アイテムはちゃんと消費されているか?そういったことを検証して、APIが正しく振る舞っているかを確認することになります。

もしAPIから返ってきた値が想定外の値、例えば、キャラがレベルアップしていないとか、アイテムがちゃんと消費されていないとかがあるとします。
その場合はテスト失敗となり、APIの振る舞いに何か異常があることが分かります。

このテストは、APIに対してだけでなくてメソッド単位でも実装します。

APIの処理は内部ではより細分化されています。
レベルアップの処理だと、キャラをレベルアップするメソッド、アイテムの数を減らすメソッド、などとわかれています。
このレベルアップの処理に対してもテストを行います。
これによって、問題が発生した時に、より細かく、どこに問題が発生したかが分かるようになります。

以上が、バックエンドに置けるテスト実装の簡単な説明でした。

これから、ファンパレのバックエンドのテスト実装の取り組みをご紹介します。

まず、具体的にどの様なテストを実装しているか、どの様な事を意識して実装しているのかを紹介します。
その後、テスト実装の現状を紹介します。
テストの実装数、メンバーの思うメリデメ等も紹介できればと思います。
最後にテストを活用した仕組みを1つご紹介させていただきます。

前提として

紹介させていただくにあたっての前提として、ファンパレではPHP、Laravelを採用しています。

また、アーキテクチャはこちらの図の通りです。
ADRについては、ほぼMVCと同じものと思っていただいて大丈夫です。

テスト実装の取り組み

それでは、テスト実装の取り組みについてご紹介させていただきます。

我々は主に3つのテストを実装しています。

  • Unitテスト
  • Featureテスト
  • Integrationテスト

それぞれについてはこの後説明させていただきます。
また、特に以下の事を重視しています。

  • なるべくすべてのメソッドを網羅するテストを書くこと
  • 不要なDBアクセスを避け、テストを軽くすること

ではまず、Unitテストについて説明します。

Unitテストはいわゆる単体テストです。
ロジックを持つような AppService DomainService Repository に対して実装します。

Unitテスト実装時のルールとしては、まず、必ず1メソッド1テスト書く必要があります。
もちろん、異常系についても網羅する必要があります。
「テストが仕様書といっても過言ではない」というのは、ルールのドキュメントに書かれていたことではありますが、テストを見れば各メソッドの仕様がわかる程度にはテストケースを網羅するべきとされています。

またRepositoryのテストについては、DBへのクエリが正しい事の検証が主な役割となる為、データストアへのアクセスを行いますが、それ以外についてはデータストアへはアクセスしてはいけないルールになっています。
これは、なるべくDBへのアクセスを減らすことでテストを軽く保つ為になります。

続いてFeatureテストになります。
こちらはAppServiceごとmockすることでロジックを除いたhttpリクエスト単位の動きをテストします。
このテストを通して、主にリクエストのバリデーションが正しく機能しているかを検証します。

例えばレベルアップする仮のリクエストを作って、Featureテストに通します。
この時、サーバーはリクエストデータが正常であるかをバリデーションします。
問題のないリクエストであれば、処理するロジックを持つServiceの部分はmockされているため、処理は正常に終わります。

異常なリクエストを渡すとバリデーションのエラーが返ってきます。
図のように「レベルを −5 増やして」というのは、強化するレベルの値は正の整数であることのチェックが通らないのでエラーになります。
異常なリクエストを正しく検証して弾けるかを検証するのがFeatureテストの主な役割になります。

Featureテスト実装時のルールです。
レスポンスについては、ステータスが200 OK を返しているかのみ確認します。
実際の返り値はUnitテストを通して検証しているため、ここでは検証する必要はありません。
そもそも、Serviceをmockしているので、そのmockの返り値を検証しても意味がありません。

最後にIntegrationテストです。
いわゆる結合テストの事で、DBにデータを入れてAPIの処理を丸ごとテストします。

Integrationテスト実装時のルールです。
Actionごと、つまりAPIごとに必ず実装します。
また、正常系のテストケース1つだけで済ませるのが望ましいとしています。
Integrationテストでは、APIの処理全体をテストするためにマスタやユーザデータなど多くのデータを挿入する必要があります。
従って、1テストケース辺りに掛かる処理時間がとても重いです。
なるべくテストを軽くするため、必要最低限のケース数にするのが好ましいとしています。

その他にも細かなルールがあります。
なるべくテストを軽く保つことは全てのテストのルールの前提として垣間見えたかと思います。

以上が、我々の実装しているテスト実装の取り組みになります。
3種類のテストを実装し、網羅性、テストの軽さを重視しています。

テスト実装の現状

ここからは、テスト実装の現状について紹介させていただきます。

まず、テストのファイル数についてご紹介します。
ざっとファイル数をカウントしてみたのですが、Unitテストは1000ファイル以上ありました。
テストケース数は7500ケース以上になります。

テスト実行に掛かる時間は12分でした。
CIなどでもテストを実行しているため、この所用時間は開発サイクルに掛かる時間に影響してきます。
現在テストの並列化などでの改善を目指しています。

本題です。
テスト実装に掛かる時間です。
やはり、しっかりとテストを実装しようとすると、とても時間がかかってしまいます。
こちらは私がテストを実装する際に掛かった時間を計ってみた例になります。
およそ20 ~ 25%はテスト実装に掛かってしまいます。

最悪の例だと、半日で実装が終わったものの、テストの実装が難しく、そちらに2日ほど掛かってしまった例も聞きました。
テストを実装するために、間違いなく実装の工数は膨れ上がります。
明確なテスト実装のデメリットになります。

これらを踏まえて、チーム内でどの様にテスト実装について捉えられているかです。
ここは私視点での感想にはなります。

まず、テスト実装は当たり前、というチーム文化が根付いています。
ルールとして定義されつつ、年単位の開発期間を経て、既にテスト実装が当たり前となっています。
その文化の根底として、現状チームメンバーが、恐らくほぼ全員テスト実装に対して肯定的な考えを持っています。
私がチームに入る前の話ですが、テスト実装については否定的だったメンバーもいたようです。
しかし現在は、その方も肯定的な考えを持つようになっています。

メリデメ踏まえて、現状バックエンドのチームでは、テスト実装をしてよかった、便利だ、というように感じているようです。

ここからは、実際のチームメンバーの声です。
チームメンバーが思う、テストのメリデメを紹介させていただきます。

まず、安心できるという声が多かったです。
正常パターン、異常パターンを確認できるので、クライアント側に渡す前にある程度実装を保証できて安心という声がありました。

次に「テストを意識して設計・実装するので、良いコードになりやすい気がする」という声がありました。
テストを意識することで、メソッドの役割が明確になり、コンパクトなメソッドになるということです。

また、実装中の動作確認がしやすいという声です。
実際、実装中にメソッド単位で小さく動作確認できるのは凄く便利と私も感じます。
テストによって、確かな実装が早く進められるというのは大きなメリットですね。

最後に、リファクタがやりやすいという声です。
例えば、仕様変更によりコードを変更した場合の対応漏れなどに気づけるといった例ですね。
リファクタだけでなく、バージョンアップの際なども、テストが通っていることで多少安心して行なうことが出来ます。

この他にも、

  • レビューの時に役に立つ
  • 他人のコードを確認する時
  • テストを見ることで理解しやすくなる

というような声も上がっています。

続いてデメリットです。

圧倒的に多い声がありました。
「テスト実装に時間がかかる事、工数がかかる事」です。
本当にテスト実装は面倒で、時間がかかります。

他にも、書き方に悩むことがあるという点を上げている方も複数人おられました。
実際、特殊な処理が必要な箇所のテストは、凄く難しいです。
そのテストをどう正しく実装するかというのは私も頭を悩ませたことがあります。
フレームワークの奥底の仕様を把握しにいって、無理やりテストを書いたりと、もちろん勉強にはなりますが、余計な時間はかかったなと思い返すことがあります。

あとは、作業効率が落ちること、テストの習熟に時間がかかることなどが上げられました。

以上が、バックエンドチームのテスト実装の現状になります。

テストの活用事例

最後に、テストを活用した事例をご紹介させていただきます。

我々のチームでは、テストを活用してN+1問題を検出する仕組みを実装し運用しています。
具体的には、Integrationテストを活用し、テスト中に流れたクエリを分析、N+1問題が起きているかもしれない疑わしい箇所を検出する仕組みになっています。

まず、N+1問題について簡単にご説明させていただきます。

N+1問題とは、データ取得時に大量のクエリを発行してしまう問題です。
この問題は、実装上の不備によって引き起こされます。

例えば、ゲームの例でフレンドのプロフィール情報一覧を取りたい場合を考えてみます。
まず、フレンドの一覧をリストで取得します。
その後、フレンドのリストをループして、順番にフレンドのプロフィールを取得しに行く。
このような実装をしてしまうと、リストを取得する時は1回のクエリで済みますが、その後のプロフィールの取得時にフレンドの人数分のクエリを発行してしまいます。
フレンドN人プラス1回のクエリが走ってしまう事から、N+1問題と呼ばれます。

この問題は、フレンドのプロフィールを取得するクエリを、フレンド1人1人に対して実行してしまうことが原因で起きます。
本来は、例えばフレンドのユーザIDをリストにして、纏めて1回のクエリで取得するべきです。

とてもシンプルな原因ですが、この問題は意図せずして起きてしまうことがあります。
詳細は省略しますが、has_manyのような、リレーションやアソシエーションなどと呼ばれる仕組みを上手く使えなかった際にN+1問題が起きてしまいます。

具体的にどの様にN+1問題を検出するのかを説明します。

テストはIntegrationテストを使用します。
テスト実行前に、クエリログを有効にしておきます。
これによって、DBに対して発行されたクエリを記録することが出来ます。
この状態で、Integrationテストを1つ1つ実行していきます。

テストが1ケース実行されるごとに、記録されたクエリを集計します。
このクエリの中にはPrepare文も含まれます。
Prepare文は、where句などに含むパラメータを含む前のクエリです。
N+1問題の特徴として、同じPrepare文に対して、違うパラメータを渡す様なクエリが複数回発行されます。
このPrepare文の重複を検出します。

最後に重複のあったPrepare文の含まれるテストをSlackに通知します。
その通知を見て、おや?N+1問題が起きているかも?と気が付くことが出来ます。

注意点として、あくまでもこの仕組みは疑わしい箇所の検出しかできません。
適切に許容された、複数回似たようなクエリが叩かれる必要のある個所についても検出されてしまいます。
また、Integrationテストのテストケースが弱く、クエリが1度しか発行されない場合にも、この仕組みではN+1問題の検出は出来ません。

しかしながら、この仕組みによって、N+1問題が起きているかもしれない箇所について洗い出すことが出来ました。
このように、テストを活用してN+1問題の検出する仕組みを運用しております。

以上で、『呪術廻戦 ファントムパレード』のバックエンドにおけるテスト実装の取り組みの紹介を終わります。
ありがとうございました。

©芥見下々/集英社・呪術廻戦製作委員会 ©Sumzap, Inc./TOHO CO., LTD.

守田 一喜

2022年新卒入社 サーバーサイドエンジニア
『呪術廻戦 ファントムパレード』にて開発に従事