次世代タクシー配車サービス「MOV」のGo活用事例

toku_bass氏(以下、toku_bass):「次世代タクシー配車サービス『MOV』におけるテスト事例紹介」というテーマで発表します。オートモーティブ事業本部、Twitterアカウントは@toku_bassです。よろしくお願いします。

(会場拍手)

社会人になってからやっていた言語は、C・Perl・Elixir・Goで、Goは1年ぐらいです。なので型のある言語は久しぶりです。ディー・エヌ・エーには入ってまだ1年経っていないです。

最初にMOVの紹介をさらっとして、どういう環境で開発をしているのか。あとは「テストを書く上でこういうことは気にしている」という方針の話と、並列テストをするために必要なテストデータの生成。残念ながらDIできないコードが存在するので、その打開の方法と、その他時間があれば発表します。

MOVの紹介です。ざっくりいうと、App StoreやGoogle Playストアでユーザアプリをダウンロードして、ユーザさんがすぐにタクシーを呼べるというサービスです。2018年4月に神奈川県限定でリリースされました。

僕が入社したのが2018年11月で、その1ヶ月後に東京でリリースされ、バンバンテレビで放送され、すごく負荷がきました。2019年7月に大阪と京都でもリリースされ、またテレビで放送され、またわりと負荷がきました。

配車アプリは「タクシー呼ぶだけかな?」という感じがあるんですけど、タクシーの中にも複数のAndroidの端末がありますし、あとは事業者様のところに管理画面があるので、それのためにやることがいっぱいあります。

あとはタクシーやユーザがどこにいるのかを管理しているサーバもありますし、いろいろなサーバがあります。今回の話は、サービス初期から存在する一般的なAPIサーバで使っているテストです。

テストを書く上での方針

環境としては、App EngineのGoの1st generationを使っていて、DBはCloud SQLの2nd generationを使っていて、あとはCloud Datastoreがあります。

テストの実行方法は、ローカルの手元のPCでgoapp testを実行する方法とGitHubのプッシュに連動してCircleCIで動くテストと、あとはchatbotE2Eです。E2Eといってもサーバで完結しているんですけど、配車系のテストをしています。

QAを依頼したものの、バグがあってQAが止まっちゃうことがあったので、そういうことをなくすために作られました。

テストを書く上での方針ですけど、今見ているテストケース以外に依存している暗黙のデータというものを用意してはダメで、テスト全体が始まる前にユーザを2人ぐらい追加しますみたいな、そういうfixtrueを使わない。あとはGoだとtesting.TのParallelがあって、単体テストが並列で回せるんですけど、これをちゃんと使っていく方針です。

テスト全体に関わるfixtureを使わないものです。マスターデータは別に使ってもいいです。マスターデータはゲームで言うところのアイテムの攻撃力や回復量などの企画で決まっているデータです。

マスターデータ以外の「ユーザを2人入れておきます」みたいなデータは、ユーザを1人追加するAPIがあってそれをテストして、「ユーザが3人になりました。OKです」というコードが書かれ始めて破滅します。

「そんなバカな」「ちょっと考えすぎだろ」みたいなことも思うかもしれませんけど、前職で10年物のすごいやつをメンテナンスしていてかなり大変だったので、本当に気を付けたほうがいいです。

並列でテストを実行するんですけど、APIのテストをDBまで共通してやろうと思うと、グローバルな状態を持っているDBがネックになってきます。なので、user_id=1みたいな固定値をテストでは書かれたくないと。今いったんは書かれているんですけど、ランダムにデータを生成するためのbxcodecさんのfakerというライブラリがあります。ざっくり言うとfactorybotみたいなものです。

このときはまだfakerに不満があって、いろいろ手を尽くすんですけど、数ヶ月前にv3が出てこれはけっこういい感じです。ユーザ定義のpluginも簡単に書けるのでこれがおすすめです。v2の頃からfaker以外のライブラリはあまり良いものがないと思っていたので、たぶんこれしかないんじゃないかなと思います。

並列テストのためのデータ生成

実際にどうやって使うのかと言いますと、Userというstructにfakerというkeyでgotagがあるんですけど、emailというgotag valueを入れておくとemailの形式で適当なemailを生成してくれます。emailはfakerの持っているデフォルトのプラグインです。とくに何も単語を書かないとString型に合わせてそのデータを生成してくれます。

ここでemail以外にも作りたいとなってきたら自分で使うgatag valueを作れるので、これでごりごり書いていけます。

MOVでの利用例なんですけど、さきほど構造体のgotagにfakerと書くと言ったんですけど、チームメンバーから「プロダクトコードにテストのための記述は嫌だ」という意見が出て、そうだと思いまして。静的にコードを解析して関数名のprefixにfakeとついている構造体を生成して、それにgotagを付けました。

あと、プライマリキーはランダムに生成すると偶然被ることもあります。前職で大きいテストを回していたんですけど、そこそこ被ります。なのでtodoなんですけど、id採番のアルゴリズムを採用して原則的に被らないようにしていこうとしています。

DIできないコードと戦う

次は「DIできないコードと戦う」。一般的にDIとよく言われるんですけど、Goではtime.Now()がどうしても撲滅することができなくて、絶対出てきます。

こいつをどうしたかというと、time.Nowの関数定義自体をどこかのpackage変数に保存しておいてラッパー関数を経由して呼び出します。それでテストのときだけpackage変数を書き換えられるsetterをビルドに含めます。この方法としてbuildタグを使っています。

ラッパーはどこのご家庭にもあるutilパッケージを作って……このnowFuncという変数にinit時にtime.Nowの関数を入れておいてプロダクションコードでutil.Now()と書いておくと、ふだんはtime.Nowが呼ばれるんだけど、テスト時はSetNowFuncという関数が使えるので、好きな固定値の時間を設定できます。

buildタグとはファイルの一番上の行にあるbuild testという記述の部分です。go testコマンドに--tagsオプションがあるのでそこにtestと書くとビルドの対象になります。

使っているところはこんな感じで、見ての通りでロックしているので、この手法を使うデメリットとしては並列で動かない点です。そこは甘んじて受け入れようかなと思っています。

tipsとして1つ言っておきたいのは、export_test.goを用意してsetterを作っていたブログを参考にして、util.Nowを作りました。だけど、うまく動かなくてすごいハマって「なんでだろう?」と思っていました。

utilに作っているので、例えばこのコントローラーのテストでutil.Nowを使いたい時にcontroller_test.goとかはビルド対象に含まれるんですけど、utilはテスト対象じゃないから、utilのディレクトリの下にexport_test.goというファイルを配置しても、それがビルドされないんですよね。なのでbuildタグで強制的にビルドに含めています。

あとは、これは完璧に力技なんですけど、Client実装をファイル単位で偽装します。これもbuildタグでやっていて、これはあまり説明したくないんですが(笑)。

シンプルなものだとこんな感じです。本来ならTagメソッドがオリジナルで呼ばれるんですが、package変数のmockTagが呼ばれるようになっていて、テスト時に、package変数に好きな関数を設定するやり方です。

これはかなりの業技なので、これを使わないといけない悲しみというのはあるんですが、「プロダクションコードでif(isTest)みたいなものは混ぜない」「リファクタリングをしないとテストが書けません」みたいなことは言わないという心意気はわかってほしいなと思います。

testeratorとpstestに関するtips

その他のTipsについて、よくあるのがtesteratorとpstestなので、その話をしようと思います。testeratorは有名なので知っている人もいるかもしれないんですけど、GAE用にテストサーバを上げると3秒ぐらいかかるんですよ。これをテストごとに上げ下げしていると、とてもじゃないけどやってられないので、ずっと上げっぱなしにするものです。

SpinUp関数を呼ぶと、すでにサーバが起動していたらそのコンテキストかサーバの情報を返してくれて、起動していなければ改めて起動をしてから情報を返してくれます。

検索すると、はてブとかQiitaが引っかかるんですけど、たまにテストサーバが落ちてまた起動に3秒かかるので「なんでだろうな?」と思ってソースコードの中を見てみると、内部カウンターを持っていて、SpinUpで1上がってSpinDownで-1になるんですけど、これが0になるとサーバが落ちるんですよ。

なので、テストの実装者が自分でSpinUpして行儀よくSpinDownを書くと落ちるんです。テストを始めるときに+1しておいてもいいんですけど、SpinDownが何故か2回書かれていることが往々にして世の中にはあるので、package全体で利用できるラッパー関数を用意しておいて、それで中央管理をしていきます。なのでSpinUpを直接呼ばないで「この関数を使ってね」としています。

最後です。Cloud PubSubのテストでpstestがすごい便利で、pstest.NewServer関数が擬似サーバを返してくれるんです。これにクライアントがリクエストを投げると結果が返ってきます。

これを見つけた経緯がエディターの定義ジャンプでPubSubのライブラリにとんだら、そこにテストファイルがあったので、「どうやってテストしているんだろう」と思って見たらpstestを使っていて、「あぁ、こういうふうに公式のライブラリを見に行くのはいいなぁ」と思って、この場で知見を共有しておこうと思いました。

ただ、BigQueryとかにはなかったです。公式のテストを見に行っても公式でも「TODO:このテストはまだできていません」とコメントが書かれている現実が8割ぐらいです。

以上です。ありがとうございます。

(会場拍手)

司会者:すみません。自分から質問です。pstestは公式パッケージ上の中に入っているパッケージみたいなやつですか?

toku_bass:そうですね。

司会者:ありがとうございます。もう1件質問が届いております。「idを採番するのにsnowflakeを使うのは?」という質問です。「他に例えばUUIDとかstandardぽいものがありますけど、そういったものはご検討されましたでしょうか?」

toku_bass:現状はUUIDじゃなくて数字で管理をしているので、snowflake的なやつがいいです。

司会者:はい。どうもありがとうございました。