背景
会社のプロダクトに使われている単体テスト・結合テストのコストが重いので、なんでこんなに重いんだろうな?と思ったのできちんと調査してみることにした。
※この記事に対する自分の回答記事は下記。junit5 + wiremockによる単体・結合テスト。
単体テスト
基本的に、SpringBootの実装はDIコンテナを主軸に置く。
メソッドの中でnewを使わないことにより、Mockへの差し替えを容易にする効果がある。
正直、この点に関してはよく守られているようだった。
しかし、社内PFが作ってくるクソファッキンライブラリがStaticで実装されていてPowerMockでも使わないとMockにできないという我々の事情が邪魔をしている。
また、JUnit5, spockで書かれているテストの可読性も課題に感じている。
テストコードを追加しようとしたとき、あまりにダラダラと長いし、見通しが悪くて嫌になることがある。
この点に関しては問題が潜んでいるように思う。
結合テスト
単体テストが1クラスについて行われる試験ならば、結合試験は複数クラスの組み合わせてテストを行う。
いつからできた文化なのか定かではないが、自分のチームではJUnit5, spockでこれを実現していた。
つまり、単体テストフレームワークで擬似的な結合試験をおこなっていたのである。
あまりに定着しているものだから、これに疑問を抱いたことが今までなかったが、翌々考えたらこれなかなか非効率なのではないだろうか…。もっとやりようがある気がする。
まとめ
今ある問題っぽいものをまとめるとこうなる。
- 単体テストの可読性をあげるにはどうすればいいのだろうか?
- PowerMockのようなライブラリを避けるにはどうすればいいか?
- SpringBootの結合テスト自動化にはどんな方法がとれるのだろうか?
上記の問題を念頭に置きつつ、まずはJUnit5についてキャッチアップしておきたい。
JUnit5
個人的に、今安定している単体テストフレームワークはJUnit5と思う。
好きなのはgroovy/spockなのだが、こちらは情報量に負けており、ある程度卓越していなければクリアできない障害が多すぎるように思える。
spockに比べれば可読性に劣るように思えるJUnit5だが、どのような機能があるのかちゃんと調べてみた。
参考1.NTTデータさんの発表資料
Spring5に関するテスト事例の発表資料。個人的なポイントは下記。
- 結合試験を支援するモジュールとして、SpringTestが提供されている
- SpringTestContextFramework(TCF)を利用することで、JUnitなどでDIコンテナが利用できる
- JUnit5の新機能について
- ネストしたテストにおける、テストメソッドのグループ化
- 「テストケースは共通の初期化処理を持つものでグループ化されるべき」
- @Nestedアノテーションをつけたクラスは@BeforeEachを参照できる(102ページ目)
- パラメータ化テスト(@ParameterizedTest)
- ValuesSourceアノテーションに対して引数を注入できる
- 他にも、Enum定数、Stream、CSVなどで値を指定可能
- アサーションAPI強化
- assertThrowsによって、try-catchなしで例外検証が可能
- ネストしたテストにおける、テストメソッドのグループ化
- Spring5の変更点について
- テストの並列実行をサポートしている
- JUnit5.3.1以上である必要がある
- SpringJUnitConfigを利用する
SpringBoot的にはおそらくアノテーションで省略される箇所もあるので注意する必要があるが、単体テストの「可読性」に関しては幾分の価値がありそう。
テストの並行実行に関しても考えたことがなかったので言われてみれば…って感じ。
読んでいて思ったが、なにもStaticクラスをそのまま使わないでもファクトリかなんかでラップしてしまえばいいかもしれない。そうすればDIできるのだから。
参考2: ExtensionModel
可読性について調べていると、ExtensionModelというワードが目に止まった。
JUnit4で提供された@Ruleなどの仕組みが廃止される代わりに提供されるようになった機能郡らしい。
CodeFlowさんでは下記のように説明している。ちなみに原文はBaeldungさんだと思う。
JUnit5の拡張機能はテストの実行における特定のイベントに関連しており、それを拡張ポイントという。
特定のライフサイクルフェーズに達すると、JUnitエンジンは対応する拡張機能を呼ぶ。
拡張ポイントは5つある。
・テストインスタンスの後処理
https://www.codeflow.site/ja/article/junit-5-extensions
・条件付きテスト実行
・ライフサイクルコールバック
・パラメータ解決
・例外処理
早い話、あるタイミングで共通の処理を差し込めるよーということだが、具体的なユースケースがあまりピンとこない。
これはまた別の記事で深堀りしてもいいかも。
やってみたいこと
上記を受けてやってみたいこと
- 「初期化処理」に着目したテストケースのグルーピング
- StaticクラスをDIできるように別のクラスにラップする
- 共通処理をNestedやExtensionで効率化する
- テストの並列実行を行う
Spring Cloud Contract WireMock
SpringTestのくだりで結合テストに関するサポートの話が出てきていたが、あれは知らずに使用していた。
思うに、結合テストの問題は外部APIなのだ。
いくらDIがあろうと、1つのControllerに対して10の外部APIがあったとしたら、それを結合するには数個のRepositoryをMock化しないといけなくなる。
そうなったテストケースの可読性の担保はなかなかに困難だし、保守も大変に感じる。
いろいろ調べた結果、良さそうと思えたのが掲題である。
https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-wiremock
つまり、接続先APIを擬似的に再現し、それに応じたjsonを返すスタブサーバを用意するということだ。
接続先がスタブなわけだから、当然本物を呼んでも問題ない。これならMockは最小限度で済むはずである。
レスポンスの管理
しかし、対応するレスポンスをどう管理するかという問題は残る。理想的にはConsumerDrivenContractsなのだが、これは実践してみた上で諦めた。
組織に合わないというか、コストが重すぎるのだ。もっと小さな組織ならうまく行った気がする。
仕方ないので接続先のjsonは自分が作って管理する前提で、簡単に結合試験で使えるかという体でドキュメントを読んでみよう。
ドキュメントには次のようにある。
If you use
https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-wiremock@AutoConfigureWireMock
, it registers WireMock JSON stubs from the file system or classpath (by default, fromfile:src/test/resources/mappings
). You can customize the locations byusing thestubs
attribute in the annotation, which can be an Ant-style resource pattern or a directory. In the case of a directory,*/.json
is appended. The following code shows an example:
つまり、src/test/resources/mappings配下のjson全部がエンドポイントとして登録されるらしい。なるほどね。
これならば、現実的にテストの運用が可能そうに思える。
例外試験
いやまて、HttpStatusによる例外はどうする。やはりMockか?
と思ったらこんな一文があった。
Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into a Spring
https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-wiremockMockRestServiceServer
. The following code shows an example:
ようは、WireMockの設定を使ってサーバを起動することができるとのこと。
これができるのであればWireMockで紹介されているような、特定のパスに対してエラーステータスを返すようなサーバがすぐに立ち上げられるじゃないか。
やってみたいこと
- WireMockつかって結合テスト!
ただ、より慎重なテストリソースの設計が必要に思える。
闇雲にテストリソースをつくるとスタブが乱立しかねないな…。