マイクロサービスの開発とテストファースト/テスト駆動開発  【Mercari Gears Lecture Series】

こんにちは、Mercari Gears事務局です!

この記事では、動画公開以来とても反響のある Mercari Gears Lecture Series #47〜#49「マイクロサービスの開発とテストファースト/テスト駆動開発」の動画の内容を記事に起こしたものです。

今回の実際の動画はこちらになります、興味があればぜひご覧ください!

MERCARI GEARS Lecture Seriesとは?

MERCARI GEARS Lecture Seriesは、株式会社メルカリをはじめとするメルカリグループ各社が、これから目指す方向や、これから取り組む技術的なチャレンジについてご紹介するエンジニア向けのレクチャー動画シリーズです。

お話する人の自己紹介

株式会社メルペイ 柴田芳樹

九州工業大学 情報工学修士 1984年 富士ゼロックス 入社後、さまざまな会社を経て2018年6月に現在のメルペイにバックエンドのソフトウェアエンジニアとして入社、主にマイクロサービスの開発を担当しています。

プライベートでは Effective Javaを さまざまな技術書の翻訳を行っています。

アジェンダ

今日の話のアジェンダですが、 まず1990年代までのソフトウェア開発、特にソフトウェアテストがどのような状況であったかを振り返ります。

その後、きょうのテーマでもあります「テストファースト/テスト駆動開発」について、私自身のテスト駆動開発としてどのような経験をしたかをお話ししていきます。

後半は、メルペイに入って初めて従事したウェブサービス開発でどのような工夫をしてウェブサービスを「テストファースト、および、テスト駆動開発」で開発してきたかについて話をしたいと思います。

1990年代までのソフトウェア開発のテスト

まず最初に、1990年代までのソフトウェア開発におけるテストがどのような状況であったかを簡単に振り返りたいと思います。

近年、テスト駆動開発は当たり前になってきていますが、実際にはテスト駆動開発そのものは2000年以降に生まれています。

では、1980年代あるいは1990年代までは「テスト」というのはどういったものだったのか?

基本的にソフトウェアのテストは手作業で実行され、結果の確認は目視が主流だったということです。

そのことを幾つかの本から簡単に紹介したいと思います。

まず、有名なMatin Flowerの『リファクタリング』という本から、Matin Flower自身がテストコードを書いてそれをどのように実行していったかに関して記述された部分です。

「テストの実行は確かに簡単になりましたしかしテストの実行は簡単になってもテストは依然として極めて退屈なものでした。 これは、コンソールに出力されるテスト結果を私がチェックしなければならないためです」

つまりMatin Flowerは、単体テストのコードを書いてそれを実行して、結果を目で確認していたということです。

次に紹介するのは Robert Martinの『Clean Code』 です。

「この10年間の間に この業界では多くのことがありました。 1997年当時、テスト駆動開発などという言葉は誰も聞いたことがありませんでした。 ほとんどの人にとって、単体テストというのは動作をひとたび『確認』したら捨ててしまうものでした。 苦労してクラス メソッドを書き上げ、それらをテストするためのその場しのぎのコードをでっちあげていたのです」

あのRobert Martinでさえ、1990年代っていうのは単体テストを書いてその結果を確認したら捨てていたと言っています。

つまり、テストを自動実行するような環境ではなかったと述べています。

次に 別の本からの紹介です。

Coders at Work』という本からですが、これは著名なプログラマーに対するインタビューを集めたものです。そのインタビューの中で『Effective Java』で有名なJoshua Blochがインタビューを受けています。

「デバッグの話をしましょう。あなたが追いかけた最悪のバグはどのようなものでしたか」

それに対して、Joshua Blochは

「彼が最初に勤めた会社で彼が開発したソフトウェアですね。 ソフトウェアのデバッグに1週間半費やした」

という話をしています。 1週間半費やしてその原因が何であったかというと、彼が書いたコードではなくて彼が書いたコードが使っていたミューテックスのライブラリにバグがあったっていうことです。

それに対してインタビュアーが

「そのミューテックスの作者がテストを書いていればバグは見つかったはずで自分が1週間半デバッグすることはなかったのにと思いますか?」

と聞いています。

その問いに対して、Joshua Blochの答えは

「頭に入れておく必要があるのはこれが90年代初期の話だということです。 十分なユニットテストを書いていないということでそのエンジニアを非難しようという気は全く起きませんでした」

つまり1990年代ではJoshua Blochでさえ作成されたソフトウェアに対して、自動テストユニットは書かれるべきだとは全く考えたことはなかった。と、述べています。

このような形で1980年代および90年代のソフトウェアのテストというのは、手作業で行い かつ目視で確認するのが主流だったということです。

柴田さんの1980~90年代のソフトウェアのテスト駆動開発の経験

それに対して私自身がどのような経験をしてきたかをお話します。

まず、1984年に大学を卒業して社会人になり、そのときに最初に開発に従事したのは「Fuji Xerox」の6060と呼ばれるワークステーションです。

これは、ハードウェアからOSを除いて全てのソフトウェアを富士ゼロックスで開発したワークステーションで、その後従事したのは「Fuji Xerox GlobalView Workstation」と呼ばれるものでした。

元々ゼロックス社のスターと呼ばれるワークステーションのソフトウェアをサン・マイクロシステムズのワークステーションへ移植するプロジェクトで、サン・マイクロシステムズのワークステーションで動作するようにしたものです。

この2つのワークステーションの開発では、当時は当然のことながら自動テストという考えは全くなく作成したソフトウェアを手でテストして目視で確認するというのを私自身も当たり前に行っていました。

その後開発に従事したのは、デジタル複合機のコントローラーソフトウェアです 。

デジタル複合機というのは皆さんのオフィスにあるような「コピー、ファックス、プリンター」といった機能を一台で提供している複合機のことですね。

1990年代に「DocuStation IM 200」というのを開発したんですけど、これはOSはSolarisでC++で開発した製品でした。

その後、2000年以降に現在の富士ゼロックス社の「DocuCentreシリーズ」の基となる 商品の開発に従事しています。

これらの2つの複合機のコントローラソフトウェアの開発は、手で動かして目視で確認するというのが当然のことのように行われていたソフトウェア開発でした。

実際に、私自身がいわゆるテスト駆動開発を経験したのはさらにその後になります。

今度もまたデジタル複合機のコントローラソフトウェア開発で、2種類の複合機のコントローラソフトウェア開発に従事しておりまして、それぞれ完全なテスト駆動開発を行っています。

「完全な」というのは、実際の機械を使わずにPC上で全てのソフトウェアを開発できるようにしたものです。

この Take-3 Take-4 の2つの開発の経験を経て、私自身は80年代90年代の手作業によるソフトウェア開発からいわゆるテスト駆動開発の経験を経ていました。なので、メルペイへ入社してからもそれらを適用する形になっています。

メルペイのウェブサービス開発の工夫

では次に、メルペイに入って初めてのウェブサービス開発でどのような工夫をしていったか を中心にお話をしていきます。

これは、メルペイにおけるマイクロサービスの構成を簡単に表したものです。

Googleのクラウドプラットフォーム上で多数のマイクロサービスが動作する形で動いてます。

その中で、gatewayと呼ばれるサービスを通して「iOSのアプリケーション」「Androidのアプリケーション」あるいは、「ブラウザーアプリケーション」からリクエストを受けてレスポンスを返すという形でサービスは構成されてます。

2018年6月に私がメルペイに入社して私自身が従事したマイクロサービスはこの絵の真ん中にある「加盟店管理用APIマイクロサービス」で、加盟店様が使う「店舗管理」あるいは「スタッフ管理」といった機能を提供するマイクロサービスです。

クライアントに当たるのはウェブブラウザーで動くブラウザーアプリケーションとなります。

このような形で「加盟店管理用APIマイクロサービス」の開発に従事することになりましたが、その開発ステップは次のようになっていました。

この開発フローは特に当時メルペイで決まったフローではありませんでしたが、私自身が実践した開発フローです。

まず、API仕様の記述についてです。

自分が担当するマイクロサービスのAPIの仕様を記述すると、当時の課題としては多くのマイクロサービスの開発が同時並行に行われてました

同時並行に行われてたために実際自分が開発するマイクロサービスをテストするのに、依存してるマイクロサービスに接続してテストすることはできませんでした。そのため、依存しているマイクロサービスに接続することなく機能を実装してテストを完了させる必要がありました。

API仕様の記述が終わった後、開発するマイクロサービスの基本的な構造・アーキテクチャを検討し、「では次にどれのテストを書こうか」と考え、最初に作ったのはe2eのテストフレームワークです。

その詳細については、この後また詳しく話をしていきます。

フレームワークを作成した後、一つずつテストコードを作成して、機能を実装して、リファクタリングを何度も繰り返して、API仕様に書いた全ての機能を実装するという開発を行ってきました。

(※ここから動画の内容は#48に続きます。)

API仕様に求められる内容

では、まず最初にAPI仕様の記述について少し話をします。

どのようなAPI仕様であってもそこに記述されなければならない事柄は基本的に同じです。 まずは、提供される機能の説明。関数呼び出し。メソッド呼び出し。あるいはRPC呼び出しでのパラメータの意味と正当な値の範囲の記述です。

事前の状態に何らかの制約があったりするのであれば、呼び出しのためにどのような制約があるのかを記述します。

さらに重要なのは、このような説明に違反した場合です。つまり、不正なパラメータの値が渡された場合、あるいは、不正な順序や不正な状態で呼び出された場合は、どのようなエラーが返るをAPI仕様としてはきちんと記述する必要があります。

API仕様の記述について

では、どのようにAPI仕様を記述したかということを簡単にお話しします。

マイクロサービスは、全てgRPCと呼ばれる「リモート・プロシージャ・コール(RPC)」を使って通信します。gRPCは.protoファイルと呼ばれるファイルにそのRPCの定義を書くことになっています。

そして、 .protoファイルはRPCの定義を書くのですが、実際にはAPI仕様としての書き方が決まってるわけではありません。

つまり、Javadoc用のタグを使って書くとかそういった決まりは全くありません。そのため 、どのようにAPI仕様を書こうかということを考えた際に基本的にはコメントとして書くということを決めました。

コメントとして書いた例がここにある例です。

これは簡単な SayHello と呼ばれるRPCを一つ持つだけの定義です。最初にサービスの説明を書くとその後に簡単にRPCの説明ですね。

実際RPCの細かい説明はリクエストの構造体の方に書かれています。そして、RPCの詳細の説明、その下にエラーの説明があります。

リクエストの構造体のほうに書く理由は、実際にどのようなパラメータが渡るかはリクエストの構造体の定義を見る必要があるため、その定義と近い場所に具体的な説明を書くという形になっています。

簡単な例ですが、ここでは実際に書いた.protoファイルはこういう形のコメントも含めて2,500行以上となってました。

では次に、こういう形でAPI仕様が出来上がったら内部構造系のアーキテクチャを決めいきます。それらが大体決まったらテストコードを書き始めていくのですが、その際にどのようなテストコードを書きたいかということを最初に考えていました。

書きたいテストコードは2つパターンがありまして、まずその最初の1つはこちらです。

prepare 準備として依存してるマイクロサービスへのRPCごとに返すレスポンスあるいはエラーの設定を行う。 そして、その後実際のテスト対象のマイクロサービスのRPCを呼び出す。 さらに、呼び出した結果を最後に検査するという形にテストコードを書きたいということですね。

図に合わせて説明しますと、以下のようになります。

  • ①でスタブとしてのRPCの振舞いの設定をFake版のマイクロサービスに設定。
  • ②で実際にテスト対象のマイクロサービスを呼び出し。
  • テスト対象のマイクロサービスが呼び出されことで、依存してるマイクロサービスを③で呼び出し。
  • ④ではテストコードで指定したレスポンスあるいはエラーが返されて、結果を処理して⑤でテストコードに②のRPCの呼び出しの結果が返ってきます。
  • それの結果が正しいか期待通りであるかというのを検査します。

まずこういう形でテストコードを書きたいですね。

そして、もう一つのパターンですが、先ほどとほぼ似ていますが少しだけ違っています。

依存先のマイクロサービスのRPCごとにレスポンスエラーの設定を行うんですが、 さらにRPCごとに渡されたパラメータを保存する設定も行うということです。

実際にアクションとして、テスト対象のマイクロサービスのRPCを呼び出しチェックとして呼び出し結果の検査も行い、さらに依存マイクロサービスのRPCへのパラメータが正しく渡されたかを検査します。

図で説明すると、①でパラメータの保存の設定もして、最後の⑥で③のRPC呼び出しによる期待通りのパラメータが渡っていったかをきちんと確認するということです。

こういう形のテストコードを書きたいですよね。 書くためにはどうすればいいかと検討した結果、次のようなフレームワークを構築することにしました。

e2eテストフレームワーク

e2eテストフレームワークと呼んでます。

あくまでもテスト対象のマイクロサービスを単体でテストするという意味でe2eです。

まずこのフレームワークの構成は大きく2つのプロセスに分かれます。

1つは左側にあるテスト対象のマイクロサービス。 もう1つは右側にあるTest Suiteと呼ばれるプロセスです。

Test Suiteはテストコードの固まりですが、さらにこのプロセスの中では依存してるマイクロサービスのフェイクサーバーが動作します。

このような構成にすることで実際のテストでは、

  • ①でRPCごとにレスポンス/エラーを設定し
  • ②でRPC呼び出しを行い
  • そうするとテスト対象のマイクロサービスが③でRPC呼び出しを行う

この③のRPCの呼び出しは依存してるマイクロサービスへの呼び出しなのですが、実際にはテスト対象のマイクロサービスを起動する際に環境変数を設定することによって右側のTest Suiteのプロセスへ接続するようになっています。

このような形でテストコードを書くフレームワークを構築しました。

この2つのプロセスの関係をもう一度、別の視点から説明しますと

まず右側のTest Suiteのプロセスが依存してるマイクロサービスのFakeを同じプロセス内に準備します。

その後テスト対象のマイクロサービスを起動します。

テスト対象のマイクロサービスは最初の初期処理が幾つかありますのでそれらの処理が終わってリクエストが受け付けられる状態になったらサービスReady通知を行います。

Test SuiteのプロセスはサービスReady通知を受けるとテストを実施します。

全てのテストが終了したらサービス終了指示をテスト対象のマイクロサービスに対して行い、テスト対象のマイクロサービスが終了するとサービス終了通知を送ります。

サービス終了通知を受け取るとTest Suiteのプロセスは終了します。

一方で、テスト対象のマイクロサービスも終了してしまいますが、その際にカバレッジも保存するようになっています。

外部で動作するサービスのモック(フェイク)方法

テスト対象のマイクロサービスを単体でテストするという形でフレームワークを構築したのですが、依存してるマイクロサービスは全て右側のTest Suiteプロセスの中で動いています。

一方で、実際には全ての依存してるサービスがメルペイ内のマイクロサービスとは限りません。

外部のサービスを使ってる部分もあります。

そのため、その外部のサービスもFakeを行う必要があります。

多くの外部のサービスはAPIが公開されてることが多いです。

さらに、Go言語を使ったアクセスが可能なサービスの場合はソースコードも調べることが可能なので、独自にFakeするのはそれほど難しいことではありません。

外部サービスのFakeを用意することによってテスト対象のマイクロサービスから外部サービスのAPIが正しく呼び出されたかをテストコードで検査することが可能となります。

実際の開発では、いくつかの外部サービスのFakeサービスを用意してます。

まず例えば GCPの「PubSub、zendesk、Slack、Google Drive」といったものです。

ただし、外部データベースである「GoogleのSpanner」などは、Fakeせずにそのまま使っています。

テストフレームワークの共通化

このような形で、私が担当した「加盟店管理用APIのマイクロサービス」の開発を行ってましたが、その後他のサービスチームへ移って似たようなテストフレームワークを導入していきました。

現在ではそれらのフレームワークを共通化して、共通リポジトリとして管理するようになっています。

それらの共通リポジトリには、各マイクロサービスのFakeサーバーが含まれてます。

これらは実際にはgRPCの定義に基づいて自動的に生成されます。それらのFakeサーバーをスタブあるいは、モックとして使用するためのAPI、テスト対象のマイクロサービスを起動するAPI、または終了させるAPI、それに加えて外部サービスの各種Fakeサーバーといったものを用意しています。

実行フロー制御ロガー

Fakeサーバーとは別に、「実行フロー制御ロガー」というAPIも用意しています。

実行フロー制御ロガーAPIというのは簡単に言うと、

>ゴルーチンのスケジューリング次第で発生する不具合を再現するためにゴルーチンの実行順序をテストコードから制御して意図したタイミングで処理を実行させる機構です。

噛み砕いて処理を口頭で簡単に説明すると、

テスト対象とテストコードの両方にログを出すというメッセージを書いた場合。

例えば

  1. 「テスト対象のコードにAというログを出力する」というコードを書いて、
  2. テストコードのほうには「Bというログを出力する」コードを書いて、
  3. さらにテストコードのほうで「ログの実行順序をA B」

このように記述して、Aを出力するログにコードが到達すると、その時点では実行が停止ます。その後テストコードで書かれたBのログの出力が実行されると初めて停止しているAのログの出力が進んで、テスト対象のモジュールの実行が再開するという仕組みですね。

これはNetBeansが提供している単体テスト用のライブラリを模倣したもので、それをe2eのテストフレームワークでも使えるように実装したものです。

この詳細については『APIデザインの極意』と呼ばれる本に書かれていますので参考にして いただければと思います。

APIデザインの極意 Java/NetBeansアーキテクト探究ノート – インプレスブックス

e2eテストフレームワークの長所と短所

こういう形でe2eのテストフレームワークを用意して、最初のマイクロサービス、あるいは 他のマイクロサービスの開発をしてきました。

このフレームワークの特徴として長所短所を簡単にまとめます。

まず長所としては、テスト対象のマイクロサービスをそのまま別プロセスとして起動できるということです。

つまり、本番用のコードをそのまま動作させることができます。

実際の内部構造には依存しませんので必要に応じて内部構造をリファクタリングしやすくなり、さらに依存しているマイクロサービスが返すレスポンスエラーをテストコードで全て設定しますのでそれらを全て網羅したテストが容易に書けます。

短所としては、ある程度ホワイトボックスなテストになってしまうという点です。

これは長所の裏返しですが、どうしてもテスト対象のマイクロサービスがどのようなRPCに依存しているか、そしてそのようにマイクロサービスに発行するかを知っていないとテストが書けないということです。

従って、純粋にブラックボックステストとして書けるのは依存してるマイクロサービスを全く呼び出さない機能の処理だけとなります。

(※動画はここから#49へ続きます)

テストファースト開発のおさらい

ここでもう一度 テストファースト開発という視点から、どのような開発をしてきたかを簡単に説明します。まずは、新規機能開発ですね。

最初にAPI仕様を作成したのは、実際に私自身2018年6月に入社して2〜3ヶ月頃です。担当するマイクロサービスのAPI仕様をずっと書いてました。

そして、ほぼAPI仕様が完成した頃に内部のアーキテクチャを決めて、フレームワークを構築して、実際の開発に入りました。

その際に、まずテストコードを作成してテストが不合格であることを確認してから実装を行い、テストを合格するのを確認してからリファクタリングする。いわゆるテストファースト開発を繰り返して行っていました。

実際の開発は、例えば不正なパラメータが渡ってきた とか、不正な呼び出しになった『エラーケース』のサイクルを先にテストコード書いて実装して、最後に正常ケースのテストコードを書いて実装するという順序で行いました。

一通りの機能開発が終わればそれで全ての開発が終わりというわけではなく、実際には機能修正っていうのが入ります。機能修正の際には、当然のことながらAPI仕様書を修正していきます。

そして、仕様を修正したらテストを作成する。もしくは既存のテストを修正するということを行います。

その後のサイクルも全く同じで、テスト不合格になって実装→テスト→合格→リファクタリングというサイクルを繰り返していきます。

それ以外のケースとしては、バグ修正があります。バグ修正の場合は当然のことながら最初にその原因の調査を行います。

原因の調査を行って原因が分かれば、まずやることが再現テストの作成です。その原因を修正したとしたら、きちんと合格するテストを書くということです。

原因をまだ修正してないので実際には再現テストは不合格になります。

その後、修正実装を行い、再現テストを合格するのを確認してからリファクタリングという流れになります。

再現テストで一番厄介な原因として、メルペイのマイクロサービスは全てgo言語で開発されていますので、goroutineのスケジューラが原因で発生する問題です。

その問題を調査した場合に、どのようにgoroutineがスケジューリングされれば問題が発生するか原因分かったとしても容易に再現することはできません。

そのため、それらを容易に再現するために先ほど説明した「フロー制御」ロギングAPIというのを用意しています。

これにより、goroutineのスケジューリングをテストコードからきちんと制御して問題を再現することができます。

このようにして、バグの修正に関しては必ず再現テストを書いてから修正するということを行っていますと次のような問題が出てきます。

Gatewayを経由したテストの例

例えば、加盟店管理用apiのマイクロサービスはそのクライアントのウェブブラウザーですので容易に不正なパラメータあるいは不正な呼び出しということを行おうと思ったら行えてしまいます。

そのため、API仕様どおり正しく動くかどうかというのをきちんとテストする必要があります。

その動作を確認するために『サービステストスイート(Test Suite)』を開発しました。

今まではテスト対象のマイクロサービスを単独でテストするという『e2eテストフレームワーク』の話をしてきました。

それで実際に開発は終わりではなく本当に依存してるマイクロサービスを接続してテストする必要もあります。

そのため、GCP上のテスト環境上に開発したマイクロサービスをデプロイしてテストする必要があるということです。

加盟店管理用APIマイクロサービスはgatewayを通してアクセスしますのでgatewayを経由したテストというのを作成する必要があります。

それらのgatewayを通して作成することでですね。

不正なパラメータ、または不正な呼び出しを検出してAPI仕様に書かれているとおりのエラーをきちんと返すかっていうことです。

初めてのウェブサービス開発で心得たこと

こういう形で最初の担当したマイクロサービスを開発してきたのですが初めてのウェブサービス開発で心得たこととして、大きく2つあります。

1つはAPI仕様をきちんと記述するということです。

APIマイクロサービスも含めてメルペイのマイクロサービスはgRPCで通信してますのでその仕様をきちんと.protoファイルに記述する必要があります。

で、どのようにAPI仕様を記述するかについては、メルカリTech Blogで簡単に紹介していますので参照していただければと思います。

2番目に心がけたこととしては、自分が作ったマイクロサービスのテストをきちんと行うということです。

特にフロントと接続することなくマイクロサービス、特にAPIマイクロサービスの場合はそのAPIをきちんとテストするTest Suiteを開発する必要があるということです。

最後にもう一つ実践したこととしては、いわゆるテストファースト開発です。

最初に説明しましたように、1980年代90年代というのは手で動かして目視で確認するのが当たり前の時代で、2000年以降テスト駆動開発というのが広まってきてます。

ただ、今日広まってきたといってもテスト駆動開発ということで実装を書いてその後でテストを書くという人も依然多いです。そうではなくて、本当に先にテストを書くというテストファースト開発の実践を心がけてきました。

こういう形でメルペイに入って初めてのウェブサービスの開発を経験したのですが、きょうの話を通して皆さんがマイクロサービスアーキテクチャに基づくソフトウェアを開発される際のいわゆるテストファースト、あるいはテスト駆動開発としてどのような工夫をすればいいかということの参考になれば幸いです。

参考資料・関連文献の紹介

最後に関連した資料を簡単に紹介しますと、先ほど紹介したAPI仕様の記述ということで Mercari Tech Blogが公開されています。

あと、ブログ記事として私自身の記事で以下の記事タイトルのブログを公開しています。

最後は、私が経験したソフトウェアテスト・ワークステーション・組込みシステム・ウェブサービスということで、JaSST Tokaiの特別講演の資料で私自身の経験が述べられています。

メルペイでの初めてのウェブサービスの開発ということでその中でテストファースト・テスト駆動開発ということを中心に、どのような工夫をしてどのようなソフトウェアを開発してきたかっていうことを簡単に紹介しました。

ご清聴ありがとうございました。