はじめに
私のチームではSpringFrameworkを使っているのですが、JUnitテストが2時間半もかかってしまい、あまりの遅さのため日々テストが早く終わってくれーという祈りとともに仕事をしていたのですが
さすがに、どげんかせんといかんということで実施した施策を以下記述いたします。
現状分析
- ComponentScan対象は全クラス(←雑すぎる・・・)
- ComponentScanだけで18秒程度かかっている
- どうやらクラス単位でコンポーネントスキャンしなおし、ApplicationContextを作り直しているということはログからわかった(←これが最大のトラップでした)
作戦1:ComponentScan対象を減らす
ComponentScan対象があまりに多いことも1つの問題だったため
スキャン対象のパッケージをがんばって減らしてみました。
@ComponentScan(
basePackages={
"jp.xxx.xxx",
"jp.yyy.yyy",
"jp.zzz.zzz"
}
)
これで、ComponentScanの時間が18秒⇒12秒程度まで減少することができました。
ただし、これは例えば上の例であれば"jp.xxx.kkk"パッケージにコンポーネントクラスを作成するとComponentScan対象にならないため、今後の開発が面倒になっていくかと思いますが、一応の効果がありました。
作戦2:spring-context-indexerを使う
spring-context-indexerは簡単に言うと、コンパイル時にComponentの一覧が記述されたインデックスファイルを出力し、それを利用することで、Spring起動時にスキャンしなくても良いファイルをスキャンしなくなるといったものです。
詳細は以下を参照してください。
設定方法は以下ドキュメントに従って設定します。
私のプロジェクトではGradle4.6以降を利用しているため以下のように記述しました。
※テストパッケージ配下にテスト用のコンポーネントファイルを作成していたため上記リンクの記法に加えて「testAnnotationProcessor 」もつける必要がありました。
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.3.21"
testAnnotationProcessor "org.springframework:spring-context-indexer:5.3.21"
}
結果としてspring-context-indexerを利用することでテスト全体として、30分程度高速化できました。
ここまで実施して、ようやく1時間半程度になったものの、せめてランチ中にテストが終わってほしく何とかして1時間を切りたいと思っておりましたが、対応するネタが無くなり困ってきておりました。
作戦3:forkEveryを見直す。
改めてSpringドキュメントを読み返すと、以下のように記述されていることに気づきました。
TestContext フレームワークがテスト用に ApplicationContext (または WebApplicationContext)をロードすると、そのコンテキストはキャッシュされ、同じテストスイート内で同じ一意のコンテキスト構成を宣言する後続のすべてのテストで再利用されます。
例: TestClassA が @ContextConfiguration の locations (または value)属性に {"app-config.xml", "test-config.xml"} を指定する場合、TestContext フレームワークは対応する ApplicationContext をロードし、それらのロケーションのみに基づくキーの static コンテキストキャッシュに格納します。そのため、TestClassB がその場所の {"app-config.xml", "test-config.xml"} も(継承を介して明示的または暗黙的に)定義し、@WebAppConfiguration、異なる ContextLoader、異なるアクティブプロファイル、異なるコンテキスト初期化子、異なるテストプロパティソース、または異なる親コンテキストを定義しない場合、同じ ApplicationContext は両方のテストクラスで共有されます。これは、アプリケーションコンテキストを読み込むためのセットアップコストが 1 回(テストスイートごとに)発生するだけであり、その後のテスト実行がはるかに高速であることを意味します。
ということは、どうやらSpringさん的には、ApplicationContextはできるだけ使いまわそうとしてくれるらしい。あれ?はじめの分析の
- どうやらクラス単位でコンポーネントスキャンしなおし、ApplicationContextを作り直しているということはログからわかった
というのが何かおかしい・・・・
と思い見直してみると、gradleの設定が以下のようになっておりました。
test {
forkEvery=1
}
forkEveryとは・・・
forkEvery:
1つのテストプロセスで実行する、テストクラスの最大数。
1 以上の値を指定すると、指定した数のテストクラスを実行した後、テストプロセスの再起動が行われる。
0 の場合はテストプロセスの再起動は行われない。
デフォルト 0L
つまり、forkEveryを1に設定するということは、1クラステスト実施するたびにテストプロセスを再起動するという設定であるため、SpringさんとしてもApplicationContextを使いまわしことが不可能だったということのようです。
以下のようにforkEveryの値を適当に設定する行うことで1時間を切ることができました。
test {
forkEvery=10
}
結論
- 不要にComponentScanですべてスキャンすることはやめよう
- SpringさんはApplicationContextを使いまわそうとしてくれるはず。使いまわさない場合は何かおかしいぞ(forkEveryなどの設定を見直そう。)
- とはいえ、まだ1時間かかっている。当プロジェクトではDBUnitなど使っているため多少仕方ないのだが・・・俺たちの戦いはこれからだ!状態ですね・・・