MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

タクシーアプリ「GO」の管理画面を「テストが書きやすい」システムにするために考えたこと

バックオフィステスト
October 31, 2022

皆さんこんにちは。バックオフィス管理画面グループの@lighttiger2505です。 前回社内ブログにNeovim v0.5の解説記事を投稿して以来、ひさびさの投稿です。時の流れは早いもので、Neovimのバージョンもいまやv0.8になっています。

本日は前回とは少し趣向を変えて、当グループで開発している「GO管理画面」というプロダクトの設計思想についてご紹介します。


GO管理画面について

本題は設計の話ですが、前置きとしてまずはGO管理画面について説明します。 GO管理画面とは、タクシーアプリ「GO」を導入いただいているタクシー事業者様が、GOを運用していくため、必要な管理機能を提供します。 プロダクト機能としてよくある管理画面のイメージが近いと思います。具体的な機能を以下に示します。

An image from Notion

GO

An image from Notion

GO

An image from Notion

そんなGO管理画面は、タクシー事業者様への公開が2022/09/10と、比較的若いプロダクトです。 GO管理画面のリリース以前、GOに統合する以前から利用されているJapanTaxiの管理画面(JapanTaxi Partner Platform)とMOVの管理画面(タクシーモニター)があり、タクシー事業者様はそれぞれが利用していた管理画面を、継続してご利用頂いているような状況でした。

そのため管理画面の開発が二重に発生しており、これを一つの画面に統合し、今後の機能追加をよりスピーディに行えるようにするのが狙いです。 一方でタクシー事業者様にしてみれば、使い慣れた管理画面が変わることになってしまうため、以前の画面よりUI/UXに優れた画面となることを、目標の一つとしています。

前の管理画面は多様な機能を用意しているため、全機能をGO管理画面に一度に移行するのは難しく、現在は前の管理画面からシングルサインオンでGO管理画面にアクセスしていただき、前の管理画面とGO管理画面の両方の画面を併用していただいている状況です。

GO管理画面のプロダクトマネージャーの友松が書いた、GO管理画面の前身の一つとなるタクシーモニターの記事がありますので、より詳しい説明をご覧になりたい方はご参照ください。

GO管理画面のシステム構成

An image from Notion

GO管理画面のシステム構成は、Golangで実装されたAPIサーバーとTypeScript+Reactで実装されたフロントエンドからなる一般的なWebアプリケーションとなっています。 システム構成やフレームワークやライブラリについては以下のとおりです。

インフラとしては、APIサーバーは弊社のk8s基盤であるKenos上にデプロイされており、フロントエンドはAWS S3とCloudFrontによって配信されています。 Kenosについては以下を参照ください。

システム構成の詳細については、今回の本筋ではないため、そのほかの詳細は割愛します。

GO管理画面を形づくる思想

ここからが本題です。

GO管理画面はレガシー?

GO管理画面の概要を見ていただいて、Webアプリのエンジニアの方であれば、よく見るシステム構成だな、と思った方が多いかと思います。GOはNo.1タクシーアプリというだけあって、車両や取引の情報の件数が、一般よりも多い点はあるものの、GO管理画面に必要な要件は一般の管理画面と言われるプロダクトと大きな差異はありません。

一方で管理画面はGOが存在し続ける限り、必ず存在し続ける必要がある欠かせないプロダクトであり、継続した保守が必要になります。そのためエッジの効いたシステム構成を取る必然性がありませんでした。

クライアントの通信手段としてGraphQLやgRPC+gRPC GatewayではなくREST APIを採用したのも、管理画面用の非公開APIは一般公開APIと比較して仕様変更に自由がきくため、GraphQLの柔軟な拡張性などは不要ですし、動作確認に専用のツールが必要なgRPCよりも多くのエンジニアがより簡単にcurlなどで叩けるREST APIにしたほうがメリットが高いと判断したからです。

「テストが書きやすい」システムを作りたい

GO管理画面の初期構想をマネージャーから相談されたとき、ある程度枯れた技術にするほうが合理的だと直感で思いました。しかし、ひとりのエンジニアとしては、面白みに欠けるとも思いました。そのため一方踏み込んで、こういったプロダクトをエンジニア観点で魅力的にするには、どうしたらいいかを考えました。

そこで私が出した結論は、 「テストが書きやすい」システムを作ろう。 でした。

プログラムを書いたら、すぐにユニットテストを書いてテストが実行できる。リポジトリにプッシュしたら即座にCIが実行され、動作を担保する。デプロイがされたらすぐさまそのデプロイ後の状況をテストして、正常動作することを確認する。そんなイメージを描きました。

そうすることで、エンジニアは機能追加や改修において、本来やるべきであるエンジニアの仕事。コードを調べ、設計をし、コードを書くという行為に集中できる。ノーストレスな環境を作ることができ、更に開発スピードが増す。まさに全方よしのシステムになるのではと思いました。

テストがないはストレスが高い?

私はプログラミングにおいて、もっとも重要なことの一つが。書いたコードをすぐ実行できることだと思っています。

テストと関係ないところでいえば、書いたコードをコードランナーを用いてエディタで実行できること。Vimにおけるvim-quickrunやVS CodeにおけるCode Runner等によって、即座にコードを実行できる環境を整備しておくこと。起動しているサーバーがコード変更を検知して、ホットリロードができる状態になっていることなどです。

優秀なエンジニアの逸話として、コードを高速で書き、テストなしの一発実行で最初から最後まで完璧な動作をする。というエピソードがあったりしますが、エンジニアの大半は凡人であり、そんなことはできません。また業務において保守するコードをすべて自分で書くケースは非常に稀です。 実際は、既存コードの挙動を調べるため、変更した結果を確認するため、様々な理由で書いたコードを実行し、その挙動を確認するはずです。 そして業務のコードを実行するのを、前述したコードランナーによって実行することは難しく、業務コードの動作チェックの最小単位は単体テストによって担保されるケースがほとんどになることでしょう。

ではもしも単体テストがないならば、コードを実行するために私達はどうすればいいのでしょうか?

APIサーバーを例にすれば、手動でAPIを実行するしかありません。しかしそれをするためには様々な前提条件があります。APIにリクエストするためにインタフェースの内容と、それにより変化する条件分岐を把握しており、ローカルで動作用DBのテストデータを整備されており、かつAPIをリクエストをおこなうツールを理解しているということです。動作確認をしてくれと言われてから、いくらかのリードタイムを必要とするのは間違いありません。またそれではCIにも組み込めません。

テストがないということは、コードを動かす行為に様々なボトルネックがつくことになるのです。

テストがない環境とは?

t_wada氏の発表「質とスピード」に寄せられる反響の多さを見るに、技術的負債とそれによって遅延しつづける開発を感じている人は少なくないように思います。

私の体感としても、単体テストが記載されており、最新に追従していており、それなりのカバレッジを維持しているプロジェクトはそれほど多くないように感じます。単体テストに加えて、APIのE2EテストやCypressなどのツールを用いたブラウザ経由の画面テスト等を含めれば更に少なくなることでしょう。

その他にも、テストできない機能としてよく挙げられるのが、外部システムに依存する機能です。外部APIをコールする。AWS S3やSQSなどのマネージドサービスと通信する等ですね。 またデプロイ可能な環境が本番環境しかなく、その他の開発環境は存在しないということもあるかもしれません。

質とスピードの話題もそうなのですが。様々な理由が私達にテストを書かないということを肯定させようとし続けます。私達はこの誘惑に打ち勝つ必要性があります。

テストを書くための設計

私がGO管理画面の具体的な目標として定めたのは、全領域でテストが作成できるという点でした。単体テストから始まって、APIのE2Eテスト、Cypressによる画面テスト。すべて含めて継続的にメンテナンスができるということです。 私はテストのスコープと役割を以下の4つに分割して考えました。分割基準は勘ですが、実装してみて、それなりに的を射ていると感じています。

  1. DBに実行するクエリのテスト
    • パフォーマンスチューニングにおけるクエリパターンの調査や修正後の確認
  2. HTTPリクエストのバリデーションやドメインロジックのテスト
    • ドメインロジックが想定通りに動くかの確認。一般的な単体テストのイメージに近い
  3. APIサーバーの一連の流れをすべて検証
    • APIサーバーの外部システムを含む挙動の確認
  4. 画面からAPIおよび外部システムまでの一連の流れをすべて検証
    • 画面を含む全ての挙動の確認

各テストが担当するスコープを図で示すと以下のようになります。

An image from Notion

GO管理画面では、これら全領域のテストを実行するため、以下のような点に気をつけて設計を行っています。

必ずテストをCIに組み込む

私がテストをちゃんと書くと決めたとき、最重要ポイントと考えたのは、テストをCIに組み込むことです。 もしCIにテストを組み込まなければ、実行方法を知らない人員が、テストをメンテナンスをせずにコミットをし、徐々にテストが古くなるリスクを、常に抱える事になります。 特にCypressによるE2EテストのCIへの組み込みは、ハードルが高いです。GitHub Actions上でアプリケーションを動作させる必要があるためです。

しかし、幸い最近はDockerなどのコンテナ技術により、CI上でテストを実行するためにDBなどのデータストアを起動したり、サーバーを起動したりといったことが比較的簡単になっており、やろうと思ってできないことはあまりありません。肝要なのは根性です。

GO管理画面では、現状本番環境へのCypress実行以外は一通りCI上で動作するようになっています。肝要なのは根性です。大事なことなので二回言いました。

DBはモックにしない

gomockgo-sqlmockなどを利用して、単体テストでDBと接続をしない実装もありえますが、GO管理画面の単体テストはDBと接続をするようにしています。 DBと接続し単体テストを記述する場合、乗り越えるハードルがモック化よりも多く、実装難易度はいくらか上がりますが、それを超えたいくつかのメリットがあります。

クエリ自体の細かい検証が可能

gomockでinterface定義された関数をモックに置き換えた場合、クエリ実行自体がスキップされます。これではクエリ自体の文法エラーなどの検証ができずテスト自体の正当性が低いと考えます。

またDBアクセスをするための関数を、すべてinterfaceにするのは、メンテナンス性が高いとは言えません。ドメインロジックがDBとやり取りする関数のIFは、外部サービス通信などと比較し、頻繁に変更される可能性が高いためです。 go-sqlmockを利用した経験がないので、確証は無いのですが、サンプルコードを見る限り、クエリの文法はチェックできたとしても、クエリ実行結果が想定結果と一致しているかの検証は難しいように見えます。特にCASEなどでクエリに条件分岐がある。検索条件次第で動的にクエリが変わる場合など、正しくクエリが動作する検証自体はできないはずです。

ローカル実行環境やCI上でのE2E実行で流用できる

これは副次効果ですが、DBテストのため、フィクスチャを用意し、テストデータを流し込む必要性があります。このテストデータは、そのままローカル環境を立ち上げる手動テスト、CI上でE2Eテストをするときなどに流用することができます。単体テストで整備されたデータを流用できるため、トータルのテストの準備コストが安くなります。

余談: SQLBoilerについて

余談ですが、GO管理画面でORMとして採用しているSQLBoilerは、現存するDBスキーマをベースに、DBのテーブルアクセスのボイラーテンプレートを提供するORMです。そのため各テーブルの簡易な検索や、レコード追加などを少量のコードで実装できるため、DBと接続する単体テストとも非常に相性が良くなっています。 この点については、今後追記する予定の記事でも取り扱う予定です。

APIに対するE2Eと画面テストのツールとしてCypressを採用

GO管理画面を開発した当初、APIに対するE2Eのテストツールとして、Cypressを利用することは想定していませんでした。APIのE2Eテストのツールで検索して出てくるツールといえば、PostmanKarateなどで、当初はそれらを採用する予定でした。しかし、どうにも手触りが悪いと感じていました。 PostmanはGUI前提のファイル定義になっているので、テキストエディタでは編集しづらいですし、Karateはどうにも定義ファイルの独自文法を覚えるのに抵抗がありました。また定義ファイルベースであると、スクリプトベースのツールに対して、特殊な状況になったときの拡張性に不安がありました。 そこで、当時すでに画面テストとして調査を開始したCypressでAPIテストができないか?ときまぐれで調べたところ、多くのプロジェクトでCypressを用いたAPIテストを実施していることがわかりました。 もしCypressでAPIテストを実施することができるなら、画面側のテストとAPIテストでそのままノウハウを共有することができます。手始めに書いた画面のテストについても、TypeScriptとCypressライブラリを用いたスクリプトはわかりやすいと感じていたこともあり、導入に踏み切りました。

次回予定

今回の投稿は、GO管理画面の設計思想について、テストを軸にお話させていただきました。 次回の投稿で、今回解説させていただいたテストのスコープの#1,2に当たる部分。Golangのテストとその手法について、より具体的なプラクティスの解説をしていきたいと思います。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!