Unit Test の改善に取り組んでみました
はじめに
prismatix 事業部で QA エンジニアをしている長友です。
今回は私の所属するチームの方がテスト改善を行ってくださったので、そのお話です。
経緯
今私のいるチームには、私以外に K さんというメンバーの方がおられます。 これまで私の所属する prismatix 事業部で、いろいろなマイクロサービスの開発に携われてきた方です。エンジニアリング力が高く、テストに関する本も出されている方で、私もその方の本を持っています。ですから話すときはよくテストの話題になります。
その方が、これまで開発チームにいた中で作っていたテストコードによるテストのやり方に課題を感じていたということで、今回その改善をすることになりました。 いろいろ試行錯誤をされて、こうしたらいいのではないかというアイデアが出てきたので、それをどうやって開発チームに実践してもらうかをやってみたことをお話します。
なお、私の所属するチームは「チームトポロジー 価値あるソフトウェアをすばやく届ける適応型組織設計」という本にあるイネイブリングチームという位置づけになっていて、ストリームアラインドチーム(うちの部署では各マイクロサービスの開発チーム)の自律性を高めることを目標に活動をしています。
課題
K さんがチーム内でお話してくださった感じている課題感は、以下のようなものです。
これまでの UnitTest の中では、mock を多用している。 そのため、
- テストクラスがちょっとした内部の変更でも壊れやすくなっている
- リファクタリングで注意が必要になり、その効果も得にくく、対応もしにくくなっている
ということが起こっている。
リファクタリングを行って、mock のシグニチャが変わった際にテストコードまで修正しないといけないことがある。本来リファクタリングで壊していないかをテストで確認するのに、そのテストコードを修正しているのは、テストが通るようにしているのだからテストが通るのは当たり前で違和感を感じる。
リファクタリングを行ってもテストコードを修正せずに、壊していないことを確認できることで、リファクタリングをしやすくなるようにしたい。
解決案
課題を解決するために、K さんがいろいろ検討してくださった案をチームで議論してみた結果、以下を試してみることになりました。
- Service の UnitTest で実際のリポジトリを使い、その他に依存性注入(DI)しているものがあれば実際のオブジェクトを使う(外部サービスについては mock を使う)
これに伴い、public メソッドを経由したテストを実行するようにする。依存するクラスに対してのテストも同じように public メソッド経由で行えば、デグレの検知もしやすくなり、リファクタリングの本来のメリットを得やすい。
ただし、public メソッド経由にすることで、ブラックボックステストになるので、レビューする側が仕様を知らないとレビューが難しくなるということがあがりました。
こちらについては、事前にテスト観点とその観点から出てきたケースをスプレッドシートに整理する。それをもってレビューアと認識合わせを行うことにすればよいのではないかということになりました。
詳細
K さんにこのブログを書くにあたって、サンプルコードとともに簡単に解決案の詳細をいただきましたので、こちらに記載しておきます。
======( K さんからいただいた詳細 )=======
スプレットシートは以下のような感じです。
テスト観点には「入力」と「出力」の2つのカテゴリがあります。 それぞれのテスト観点に考えられるケースを記載します。
テストケースは列毎に表現します。 例えば「Case1」では、
- payment の status が AWAITING_AUTHORIZATION
- payment.meta.status が AUTHORIZATION_REQUESTED
の時にテスト対象メソッドを呼び出すと、
- 正常レスポンスを返す
- payment の status を
CANCELLED
, meta.status をCANCELLED
に更新する
ことを表します。
このスプレットシートだけ見てもわからないかもしれませんが、コンテキストを共有できる開発者同士であればこれとテストクラスを見ればだいたい把握できるだろうという前提のもと、あまり時間をかけずに認識合わせができる資料を作ることを目的にしました。
実際のテストクラスは以下のようなイメージになります。
@DisplayName("XXXService の UnitTest") class XXXServiceTest { private XXXService sut; @Nested @DisplayName("呼び出し時の payment の status が AWAITING_AUTHORIZATION、payment.meta.status が AUTHORIZATION_REQUESTED の時のケース") class Case1 { @Test @DisplayName("レスポンスの検証") void testResponse() { ... } @Test @DisplayName("payments 更新に対する永続化の検証") void testPaymentTable() { ... } } @Nested @DisplayName("呼び出し時の payment の status が AWAITING_AUTHORIZATION、payment.meta.status が AWAITING_AUTHORIZATION の時のケース") class Case2 { ... } }
テストケース毎に Nested で class を作り、スプレッドシート側とテストケースを合わせるようにします。 期待結果の観点毎にテストメソッドを作成して、テストメソッドで何を確認するかぶれないようにしています。
レビューはスプレッドシートとテストクラスを一緒にレビュー対象にします。 実装前にスプレッドシートを作ったほうが早めに認識違いに気付けるとは思うのですが、今の所タイミングは任意です。 この方法だと、テストクラスとスプレッドシートがずれる可能性はあるのですが、そこはレビューで見ていこう、という運用です。
======( K さんからいただいた詳細 おわり)=======
展開の工夫
実際のオブジェクト(実オブジェクトと以下記載)が使えるかの確認は、解決案を検討している段階で K さんの方で実証済みでした。
ただ、実際に開発をしている Dev チームの方に使ってもらえるかが大事なので、どう展開するかをチームで話して考えた工夫の内容は以下です。
- 複数ある Dev チームの全部にいきなり展開するのではなく、どこか1つのチームに実践してみてもらう。
- やり方を伝えてやってみてとするのではなく、実際の案件で K さんが先行して実践してみる。
- K さんが実践した内容を見せながら、説明する。
- チームのどなたかに、実際の案件( K さんの対応したものに続く対応のもので実践)で試してもらう。
- K さんだけでなく、チームの他の方がレビューアとなってレビューを行う。
これらは最終的に整理した内容で、実際には、試して、その状況を週次のチームミーティングで聞く。それを受けてチームのみんなでいろいろ議論してこうしてみてはということを繰り返して実践していったものです。
展開後
現時点では1つのチームの1つの案件で実践してもらっている状況ですが、実際に体験された方からは以下のようなコメントをもらっています。
- mock を使っていたときには当たり前だと思っていたが、いろいろと書き換えたりするところが多かったので結構大変なことしているんだと認識できた。
- テストコードを書く前にテスト観点やケースをあげることで、テストの抜け漏れなどに気づけてよかった。
- 実際に先行事例を作ってもらえたので、それを参考にして作りやすい。このあとも実践していきやすい。
- 実オブジェクトを使ったテストで確認ができているので、このあとの Integration Test ではテストが減らせる。
など、良い印象を持っていただけています。
今回実際に試していただいた中では、こちらから例示していたもの以上にしっかりとしたテスト観点やケースをスプレッドシートに整理してもらえていました。 単にフォーマットに埋めるということではなく、実践された方々が自律的に工夫を加えてくださっていたのがとても印象的でした。
これも K さんが単にやり方の説明だけでなく、やってみせて、それをもとに説明を行い、実際にやってみたものをレビューしてくださるなどがあったからだと。 K さん、本当にありがとうございます。
また引き続き、イネイブリングチームでは、いろいろとストリームアラインドチームの皆さんに自律的に動いてもらえるようにしていきたいと思います。
最後に
prismatix について知りたい方は以下をご覧ください。
prismatix(プリズマティクス)事業部のことがよくわかるWebページやブログエントリ、YouTube動画 n選 | DevelopersIO
また prismatix では一緒に働いてくださる仲間を募集中です。