静的解析のメリットとデメリット

岸直輝氏:では、ここからは今お話ししたShift Leftの具体的な取り組みについて見ていきましょう。ここでは、各フェーズごとに対応する取り組みを、静的解析、ユニットテスト、E2Eテストの3つに分けて紹介します。

まずは静的解析について。静的解析は、プログラムを実行することなく、静的にさまざまな異常を検出する手法です。静的解析にはいろいろとメリットがあります。例えば、静的解析はソースコードを対象に解析を実行するので、プログラムや自動テストを実行した時に通過しないコード、実行されないコードを検証することができます。そのため網羅性が高い解析です。

また、静的解析は今回紹介するものの中で最もフィードバックサイクルが早いです。実際にアプリケーションを立ち上げる必要がないので、高速に実行できますし、IDE上に表示できるものもあり、そういったものはコーディングの最中に問題を発見できます。今回紹介する取り組みの中では一番修正コストが低いものになっているので、このタイミングで不具合を発見できるととてもうれしいですね。

一方でデメリットも存在します。静的解析が得意としているのは、言語やフレームワークに起因するようなハマりやすい罠を発見することです。公式であったりOSSで提供されているものは、汎用的に作られているものが多いので、チーム独自のルールなどに対して検証できる静的解析ツールがOSSで存在するとは限りません。ここに関しては、ちょっとデメリットではありますが、ここに対するカバーは後ほど紹介します。

私たちがメインで使っている静的解析ツールは、SpotBugsと呼ばれるツールを使っています。SpotBugsはOSSで提供されているJava向けの静的解析ツールで、多くの怪しいコードを発見してくれるツールです。

(スライドを示して)スクリーンショットを掲載しているのですが、ここではnullになり得る変数のメソッドを呼んでいるので、「NullPointerExceptionになるかもしれませんよ」という警告が表示されています。また、返り値を無視しているので「ここでこのメソッドを呼んでも意味がないコードになっているのではないか」という報告も表示されています。

他にも多くのパターンを検出してくれるのですが、そのパターンの一覧は公式ドキュメントに日本語訳されて載っているので、興味がある方はそちらを読んでみるとよいかと思います。

チームの方針として「nullをできるだけ使わない」を掲げた理由

先ほどのスクリーンショットでnull周りの検出結果を出したのですが、Javaで開発をする上で、null周りの問題、特に「NullPointerException」と呼ばれる例外に関しては、常にまとわりつく問題の1つです。

実際に私たちの開発の中でも、このヌルポ周りの問題が後々見つかり修正に追われました。こういった課題は、ドメイン知識に関する問題ではなくJavaの言語仕様の問題なので、静的解析のレイヤーで防ぐのが筋が良いと思っています。そこで、私たちのチームではnull周りの問題に対する方針をチームで決めています。

私たちのチームでは、どちらかと言うとnullをできるだけ使わないという方針に寄せています。デフォルトでは変数にnullを入れられないとしていて、nullを入れたい場合は、明示的にアノテーションを付けることを必須としています。

このような方針にした理由はいくつかあります。まず、定義される変数の多くはnullを受け入れる必要がありませんでした。ほとんどの場合、きちんと値が入るような変数でした。

変数を使うたびに「この変数ってnullが入るのだっけ?」みたいなことを考えるよりも、nullを許容しないとしたほうが、コーディング中に考えることが少なくてわかりやすいのではないかと考えました。またこの方針にすると、SpotBugsやIntelliJといったツールを使った静的解析で、このnullの周りのエラーを検出しやすいというメリットも同時にありました。

(スライドを示して)こちらは実際にnull周りの不具合が警告されている画像なのですが、IntelliJ、SpotBugs、WARNなどのエラーがそれぞれ表示されることがわかるかと思います。これで事前に気づくことができるようになっています。

静的解析のルールを自作してコーディングルール遵守を確認

さて、先ほども紹介したように、静的解析ツールはとても便利ではあるのですが、チームに特化した問題を検出するのは少々苦手としています。例えば、私たちのチームでは独自のコーディングルールが存在します。先ほど話したnull周りのものも独自のルールと言えるかなと思います。

こういったルールは、過去の過ちや経験から制定、策定されているものが多いのですが、そういったルールに対してドンピシャの静的解析ツールがOSSで提供されているとは限りませんし、なかなかありません。

そのため、今までこういったコーディングルールは、人間がレビューをする時にきちんとそのルールを満たしているかを確認していました。

しかし、それでは毎回レビューすることになり時間がもったいないし、やはり安全にプロジェクトを進めていくためには、必ずそのルールが守られているということを検証したほうがよいのではないかと考えました。そこで私たちのチームでは、静的解析のルールを自作することにしました。

ルールを自作するにあたり、私たちはArchUnitというライブラリを導入しています。これは名前のとおり、アーキテクチャをユニットテストするためのOSSライブラリです。一応名前としてはユニットテストなのですが、やっていることは静的解析なので、ここのセクションで紹介しています。

このArchUnitは、Javaのソースコードを解析した結果に対して自由にロジックを記述することができます。Javaでロジックを書けるので、比較的自由にいろいろなロジックを作り込むことができてとても便利です。また、標準で依存関係を検証する便利メソッドも用意されているので、簡単に使いたいという場合にも使えるものになっています。

ArchUnitの活用例

実際の活用例について見ていきましょう。この例は、レイヤーのアーキテクチャの依存関係を検証するようなコードの一部になっています。ControllerとInfrastructureの2層しかないのですが、ControllerはInfrastructureからのみ依存される、といった検証を宣言的に書くことができます。私たちのチームではレイヤーがもっと多いのですが、今回は紙面の都合上少し少なめに紹介しています。

先ほどの例では、標準で用意されたメソッドを使ったのですが、自由にロジックを作り込むこともできます。ここでは詳細なコードの説明はしませんが、静的解析をした結果が入っている変数に対して、いろいろと条件をつけてアサーションをすることにより、独自のロジックを実現しています。

静的解析を使うことで得られた成果

このように、私たちのチームでは静的解析をさまざまなところで使っていました。その中でいろいろな成果が得られたのかなと思っています。まず、今回のタイトルでもある安全や効率という面では、やはりnull周りの不具合をリリース前に気づけるというのは非常に良いと思っています。

やはりコーディング中に見つからずに、PRを出してCIが落ちてから気づくこともけっこうあったので、そういった面で役に立っているという実感があります。また、レビュワーがコードを見る前にCIが落ちていれば、PRの作成者がそこに気づけるので、そのやり取りの工数の削減にもつながっていると思います。

また、意図していなかったメリットもありました。今回コーディングルールの検証を静的解析で行い、CIで強制していることによって、新しくチームに参加した人がコーディングルールを知る機会となりました。

今までもドキュメントを用意しており、それを見せるなり、見つけてもらえればコーディングルールを知ることはできていたのですが、やはりどうしても伝えきれない部分がありました。しかし今ではCIによる強制力のおかげで、継続的かつ自動的にコーディングルールの存在を広めていくことができます。

また、null周りがけっこう厳格化されたことにより、自信を持ってコードを書くことができるようになりました。Perlのコードベースでは、未定義値の扱いがけっこう不安で、コードベースを読み込む時間が多かったのですが、Javaになり、加えてnull周りも独自のルールで厳格化したことによって、そういった時間が減りました。このように静的解析を入れたことにより、さまざまなメリットを得られました。

ユニットテストのメリット・デメリット

次の項目はユニットテストです。まずは、ユニットテストを含めた自動テストのメリットをおさらいしておきます。自動テストは、品質を担保する上で非常に重要な要素の1つです。一番のメリットは、テストを書くことで仕様が正しく実装されているかを検証できることです。テストコードが間違っていれば元も子もないのですが、テストを正しく書いていればその仕様を担保することができます。

また、レバレッジが効くのも大きなポイントです。仕様が変わらない限り、1度書いたテストコードは将来にわたり「その仕様が満たされているか」を担保し続けてくれます。手動テストだと毎回人間の工数が必要ですが、自動テストは必要ありません。その点で自動テストは優れている取り組みかなと思います。

私たちはユニットテストを積極的に書いています。私たちのユニットテストは、主に単一のクラスを対象にするテストになっていて、正常系、異常系を含め、網羅的に検証できるテストを書いています。

ユニットテストのメリットは、E2Eテストなど他の自動テストに比べてテストコードを書きやすい点だと考えています。ユニットテストは基本的に依存関係がある程度限定されている状態で書くものなので、考える範囲が狭いです。そのため、比較的容易にテストコードを書くことができます。

ではどれだけのテストコードが実際書かれているのか。今回の発表に合わせてテストカバレッジを計測してみたのですが、一番のドメインのロジックを含むパッケージでは90パーセントを超えていて、かなり多くのテストコードを書いている印象があります。

全体で見ると67パーセントということで、まだまだ他のパッケージでは書く余地があるかもしれませんが、ここまで高いのは自分でも驚きでした。

また、静的解析ほどではないにしろ、比較的早くテストのフィードバックを受け取れる点もメリットです。テストの実行結果に応じて随時コードを書き換えながら開発を進めていけるというのは、とても安心感があります。

一方デメリットとしては、本番環境とは異なる依存や設定値が使われている場合があるので、本番環境とどれだけ似た環境でテストができているかという点では劣ってしまうこともあります。とはいえ、品質を担保する取り組みの中では丁度いい塩梅の取り組みなのではないかと考えています。

ユニットテストを書く時に意識していること

ユニットテストを書く時に意識していることはいくつかあるのですが、リファクタリングしても壊れない書き方をすることを特に意識をしています。

リライトプロジェクトを進める中で、既存のコードベースにリファクタリングを加える機会は何度かあったのですが、その過程でバグを埋め込んでいないのにもかかわらず、多くのテストがFAILしてしまうという状況が何度も発生しました。その度に時間を割いてテストを修正する必要があり、非効率な時間の使い方をしている印象がありました。

FAILしたテストケースをよく調べてみると、その多くがモックやスタビングを多用しており、依存関係の呼び出し方を少し変えるだけでFAILしてしまうテストが、数多くありました。

こういった背景もあり、現在では同様の検証はしつつも、リファクタリングをしても壊れにくいユニットテストの書き方をすることによって、仕様の担保だけでなく現在と未来を含めた工数削減のための工夫を行っています。これも安全と効率の両立と言えると思います。

壊れにくいユニットテストを書くための工夫とは?

具体的な工夫を見ていきましょう。先ほども話したとおり、FAILの要因はモックやスタビングが原因でした。そこで、できるだけモックは使わないようにして本番環境と同じ実装を使うか、もしくはFakeと呼ばれる本番実装と同じような挙動をする、簡略化された依存をDIするようにしています。

例えば、私たちはデータベースとしてDynamoDBを使っているのですが、ユニットテストでこのDynamoDBを呼び出す部分は、ConcurrentHashMapを使ったFakeオブジェクトをDIするようにしています。

実際には楽観ロック等を考慮するためにもう少し細かい実装を作り込んでいるのですが、簡略化するとこのようなコードになっていて、単純にこのConcurrentHashMapのインスタンスに対して、データをputしたりgetしたりする実装になります。

このFakeの実装をDIすることで、そのテストコードではスタビングの記述をすることなく仕様の検証をすることができます。Fakeを使わずにテストを書こうとすると、(スライドを示して)ここでコメントアウトをしているwhenから始まる挙動をインラインで記述しなければなりません。

もしリファクタリングによってこのリポジトリのインターフェイスが変わったり、そもそも呼び出すメソッドが変わったりしてしまうと、このwhenから始まるコードがあった場合、このテストはFAILしてしまいます。

しかし、Fakeを単純にDIする実装にしておけば、ここでスタビングに関する記述をする必要がなくなるので、仕様が変わらない限りそのテストコードを修正する必要がなくなります。

その他のFake利用例

他にもいくつかの箇所でFakeを使ったユニットテストがあるのですが、ここでは外部APIの呼び出しのFakeを紹介したいと思います。

私たちがリライトしているマイクロサービスでは、他のマイクロサービスのAPIを叩きます。しかし、他のマイクロサービスを起動させてユニットテストをしたくはありません。そこで、OpenAPIのexampleに書かれた値を返すFakeのAPIクライアントを自動生成してくれる仕組みをチームメンバーが実装してくれました。

このOpenAPIは、YAMLの仕様を変更する時は基本的にexampleも追従しないとマージできないCIを作っているので、常に最新の挙動に準拠したAPIクライアントが生成されます。

こういったFakeを使って工夫をしていくことで、今までと同様の検証項目をテストしつつも、モックを記述したりテストを修正したりという工数をかなり削減することができました。

Perl独自のテストをJavaに向けて実行する仕組みを実装

さて、ここまではモックについての話だったのですが、テストコードの有効活用という観点でおもしろい取り組みをしているので簡単に紹介したいと思います。

Perlでの旧実装では、Plack::Testと呼ばれるライブラリを使って書かれたテストが数多くありました。これはPerlの、Webサーバーの仕様に準拠したサーバーにリクエストを送って、そのレスポンスを検証するテストを支援するライブラリになっています。

私たちは、安全にリライトを進めるにあたり、新実装と旧実装の差分をできるだけ少なくしたいと思いました。そのために、このテストを有効活用して、これをそのままJavaのサーバーに対して実行できないかと考えました。

このアイデアを元に、チームメンバーがPlack::Testを使って書かれたテストをそのままJavaのサーバーに対してコードを書き換えずに実行する仕組みを作ってくれました。このテストは単純にHTTPリクエストを送るのではなく、そのPerlのサーバーの仕様に紐づいている実装だったので、エンドポイントの向き先を変えればいいという単純な話ではなかったのですが、プロキシを挟むことでいい感じに実装してもらいました。

これにより、旧実装で担保していた仕様は新実装でも担保できている、ということを確認できるようになりました。これはリライトプロジェクトならではの工夫なのではないかなと思います。

E2Eのメリット・デメリット

最後はE2Eテストです。私たちはPuppeteerを使っています。Puppeteerはライブラリで、Chromeのブラウザを起動してシナリオを実行するテストのことをE2Eテストと呼んでいます。

メリットとしては、やはり本番環境と同様の環境でテストできることが挙げられます。各種クラウドサービスや他のマイクロサービスの通信を加味したテストができるのは、非常に魅力的です。

一方で、E2Eテストはかなりコストが高い取り組みです。私たちのE2Eテストは、先ほどお話ししたとおり、ブラウザを起動して検証する部類のものなので、テストの実行時間は長くなってしまいます。そのため、結果としてフィードバックサイクルが遅くなってしまいます。

また、実装工数もユニットテストよりかかるので、工数面でも課題があります。ほかにも、外部要因による影響がけっこう大きくなってくるので、よくわからないけれどFAILすることも起きたりします。いろいろとつらいこともあります。

また、Shift Leftという考え方に従うのであれば、そもそもこういうE2Eテストではなく、ユニットテストなどフィードバックサイクルが早いものにテストを寄せていきたいという気持ちもあります。

とはいえ、ブラウザやマイクロサービスとの通信を加味したテストがないというのはなかなか厳しいので、そこのバランス感にけっこう悩みました。私たちは、基本的に正常系のみE2Eテストを実装して、その他の異常系は実装しないという方針にすることにより、メリットとコストのバランスの両立を図っています。

新実装と旧実装の差分がないことを検証する仕組み

このPupperteerのE2Eテストなのですが、単純にシナリオを実行するだけではなく、新実装と旧実装の差分がないことを検証する仕組みも作っています。

私たちのE2Eテストは、Pupperteerを使ってシナリオを実行して、そのレスポンスやHTMLを比較する部類のものです。このシナリオを、新実装と旧実装の両方に対して実行します。そして、そのテストの結果やHTMLを比較して、そこに差分がないかを検証します。こうすることで、Pupperteerで書いた仕様がリライト前後でも変化しないということを確認できるようになりました。これもリライトプロジェクトならではの工夫なのではないかなと思います。

静的解析・ユニットテスト・E2Eテストの比較でわかること

さて、ここまで3つの取り組みについて紹介してきました。まとめとして、それぞれの特徴を表でまとめています。こうして見ると、いろいろと見えてくるものがあるかなと思います。

まず、このフィードバックサイクルを見てみると、静的解析が最も早く、E2Eテストが最も遅いです。Shift Leftの考え方に従うならば、フィードバックサイクルが早い静的解析だけでよいのではないか、という考え方になってしまうかもしれません。

でも、その考えはあまりよくないと考えています。(スライドを示して)この下の検証項目や検証対象を見ているとわかりますが、それぞれの取り組みにおいて、検証項目や検証対象はまったく異なります。

検証項目を見ると、どれだけ踏み込んだ検証ができているか、という点で異なっていることがわかります。ユニットテストではドメインロジックを深く検証することができますが、その他のものではそこまで踏み込んだ検証はできません。

また、検証対象を見てみると、E2Eテストでは、ブラウザや外部APIを含めた本番と同じような環境でテストすることができますが、その他では、ある程度限定された状況、もしくはそもそも実装しないで検証するというかたちになっています。これは検証対象の幅が異なるという捉え方ができるかなと思います。このように、ものによって検証項目の深さや幅が異なってきます。

それぞれの取り組みによって、得意なところ・不得意なところがあります。フィードバックサイクルを早めていくことも大事ですが、安全性が疎かになっては元も子もありません。それぞれの長所をしっかりと把握して、お互いに補完しあって安全性を高めつつも効率のいいものはどれなのだろうと探っていくことがとても大事になってきます。

「成果を定量的に計測すること」と「他サービスへの展開」に取り組みたい

では最後になりますが、今後の展望とまとめをお話ししたいと思います。今後の展望です。ここまでさまざまな取り組みを紹介してきましたが、今のところ、それらの取り組みの成果を定量的に計測することができていません。

明らかによくなっているという自覚はあるのですが、前と比較してどれだけ安全になったのだろう、どれだけ効率よく開発進められるようになったのだろうという点においては、肌感覚でしかわからないという現状があります。これを定量的に評価する仕組みができれば、今後よりよい改善や、よりよいネクストアクションにつながっていくのではないかと考えています。

また、今回の取り組みを他のサービス、他のプロダクト、他のリポジトリへ広げていくということもあまりできていません。今回話したテクニックは、静的解析やテストにおけるものなので、基本的に他のサービスにも応用が可能です。しかし、今のところ私たちのチームのリポジトリでしか適用できていません。

チーム全体で見た時に、他にもJavaで書かれているマイクロサービスがあるので、そういったものにおいて、横展開をするのは比較的簡単なはずです。

こういった取り組みを広げていけば、もっと多くの開発者が快適に開発できる環境を作れるのではないか、と考えています。

Shift Left戦術のまとめ

ではここまで話してきたお話をまとめましょう。このセッションで一番持って帰ってほしいのは、Shift Leftという考え方です。何事でも早期に発見し、改善のフィードバックサイクルを回していけば、今までトレードオフと捉えていた安全と効率を両方解決できる可能性を秘めています。もしみなさんが運用しているサービスの中で、こういった考え方が適用できるポイントがあれば、とてもうれしいなと思っています。

また今回の発表では、私たちのチームの取り組みとして、静的解析やユニットテストや、E2Eテストを紹介しました。これらのテクニックは、リライトプロジェクト特有のものも多かったので、直接視聴者のみなさんの役に立つかどうかはわかりませんが、特性の異なる複数の取り組みを組み合わせることにより、検証の範囲、幅、深さを広げていくという、考え方自体はさまざまなところに応用可能だと思っています。

ぜひ、今回の発表がみなさまの参考になれば幸いです。(スライドを示して)参考文献はこちらです。これで私の発表は以上にしたいと思います。本日はどうもありがとうございました。