はじめに
Spring BootでCommandLineRunnerを使ってバッチアプリケーションを作成したとき、@SpringBootTest
をつけたテストを走らせるとテスト動作前にCommandLineRunnerのrunメソッドが走るということが起きました。
この記事はこの原因解明と回避策を書きます。
前提
ライブラリ | バージョン |
---|---|
Spring Boot | 2.1.6.RELEASE |
ソース
今回のソースコードです。
https://github.com/kawakawaryuryu/command-line-runner-sample
どんな実装をしたか
ざっくり以下のような実装をしました。
@SpringBootApplication
public class SampleApplication {
public static void main(String... args) {
SpringApplication.run(SampleApplication.class, args);
}
}
// CommandLineRunner実装クラス
@Component
@Slf4j
@PropertySource("config.properties")
public class JobLauncher implements CommandLineRunner {
private final String hogeValue;
public JobLauncher(
@Value("${hoge.value}") String hogeValue) {
this.hogeValue = hogeValue;
}
@Override
public void run(String... args) {
log.info("app started");
}
public String hoge() {
log.info(hogeValue);
return hogeValue;
}
}
hoge.value=hoge
今回のテスト対象メソッドであるhogeメソッドは本来もっとちゃんとした処理を行っていますが、ここではあえて簡略化しています。
そしてテストクラスは以下になります。
@RunWith(SpringRunner.class)
@SpringBootTest
public class JobLauncherTest {
@Autowired
private JobLauncher jobLauncher;
@Test
public void testHoge() {
String hoge = jobLauncher.hoge();
assertThat(hoge).isEqualTo("hoge");
}
}
これを実行すると、テスト実行時のSpring起動ログ(一部)はこのように表示されます。
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
...
2019-07-27 22:56:54.479 INFO 49823 --- [ main] c.k.c.JobLauncherTest : Started JobLauncherTest in 1.48 seconds (JVM running for 3.023)
#↓hogeメソッドの前にCommandLineRunnerのrunメソッドが走っている↓
2019-07-27 22:56:54.481 INFO 49823 --- [ main] c.k.commandlinerunnersample.JobLauncher : app started
2019-07-27 22:56:54.892 INFO 49823 --- [ main] c.k.commandlinerunnersample.JobLauncher : hoge
...
分かりますかね?そう、なんとJobLauncherTestクラスのテストが走るよりも前にCommandLineRunnerのrunメソッドが実行されているではありませんか。
今回はログを吐いているだけの処理なのでテストは成功しますが、JobLauncherが色々なクラスに依存していたりするとテストが難しく困ってしまうというわけです。
原因
調べてみるとCommandLineRunnerに関して公式ドキュメントにこんな記述が。
If you need to run some specific code once the SpringApplication has started, you can implement the ApplicationRunner or CommandLineRunner interfaces. Both interfaces work in the same way and offer a single run method, which is called just before SpringApplication.run(…) completes.
つまりCommandLineRunnerはSpringApplication.runが完了する直前に呼ばれると。
また、@SpringBootTest
はSpring Bootの機能を使ったテストができるアノテーションですが、SpringApplicationによってApplicationContextを生成します。
Spring Boot provides a @SpringBootTest annotation, which can be used as an alternative to the standard spring-test @ContextConfiguration annotation when you need Spring Boot features. The annotation works by creating the ApplicationContext used in your tests through SpringApplication.
これらの理由からテスト前に呼ばれると思われます。
解決策
調べてみると何通りか解決策はあったのですが、ここでは自分が推奨する@ContextConfiguration
アノテーションを使った方法について書きます。
以下のような実装をして無事解決しました。
@RunWith(SpringRunner.class)
@ContextConfiguration(
classes = TestConfiguration.class,
initializers = ConfigFileApplicationContextInitializer.class)
public class JobLauncherTest {
@Autowired
private JobLauncher jobLauncher;
@Test
public void testHoge() {
String hoge = jobLauncher.hoge();
assertThat(hoge).isEqualTo("hoge");
}
}
@ComponentScan
@Configuration
public class TestConfiguration {
}
解説
@ContextConfiguration
@ContextConfiguration
アノテーションはSpringの機能を使ったテストを実現できるアノテーションです。@SpringBootTest
との違いはSpring Bootの機能を使ったテストが行えるかどうかにあります。
原因のところでも書いたように@SpringBootTest
はSpringApplicationによってApplicationContextを生成するのに対し、@ContextConfiguration
は指定したConfigurationからApplicationContextを生成するのでSpringApplicationは一切使いません。
よってCommandLineRunnerのrunメソッドが呼ばれる心配もありません。
@ContextConfiguration
のinitializersフィールド
@ContextConfiguration
のinitializersには特定のApplicationContextInitializerを指定することができます。
ApplicationContextInitializerとはApplicationContextの初期化時に特定の処理を挟み込むことができるものです。
今回はConfigFileApplicationContextInitializerというイニシャライザを挟んでいます。
ConfigFileApplicationContextInitializer
ConfigFileApplicationContextInitializerはapplication.propertiesやapplication.ymlから値を読み込んでEnvironmentに格納してくれるイニシャライザです。
これによって@SpringBootTest
でなくてもSpring Bootの機能である外部設定ファイルの読み込みを利用してテストを行うことができます。
今回はこれを指定することでapplication.propertiesからの設定値の取得を実現しています。
TestConfigurationの@ComponentScan
@SpringBootTest
は@SpringBootTest
のついたテストクラスのパッケージから上っていって@SpringBootApplication
もしくは@SpringBootConfiguration
を含むConfigurationを探し、それをもとにApplicationContextを生成します。@SpringBootApplication
は@ComponentScan
を含んでいるので@SpringBootTest
使用時は基本的に特にConfigurationを指定せずともBeanのスキャン、登録がうまくいきます。
一方@ContextConfiguration
はそういった機能はないので自分でBeanをスキャンして登録する必要があります。そのため、今回は@ComponentScan
をConfigurationに付与し、それを@ContextConfiguration
に指定することでBeanのスキャン、登録を行っています。
謎な点
Spring Bootの公式ドキュメントのConfigFileApplicationContextInitializerのページを見ているとこんな記述もありました。
Using ConfigFileApplicationContextInitializer alone does not provide support for @Value("${…}") injection. Its only job is to ensure that application.properties files are loaded into Spring’s Environment. For @Value support, you need to either additionally configure a PropertySourcesPlaceholderConfigurer or use @SpringBootTest, which auto-configures one for you.
すなわち、ConfigFileApplicationContextInitializerだけでは@Value
によるインジェクションはできず、インジェクションしたいのであればPropertySourcesPlaceholderConfigurerを設定するか、Auto Configureしてくれる@SpringBootTest
を使う必要がある、と書いてあります。
しかし、自分が試した@ContextConfiguration
とConfigFileApplicationContextInitializerだけでも@Value
によるインジェクションが行われました。
ここだけは未だになぜインジェクションできたかわかっていません
もしかするとSpring 4.3からはPropertySourcesPlaceholderConfigurerを明示的に指定しなくても良いらしいのでそれが関係ある。。?(by Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発 | 株式会社NTTデータ |本 | 通販 | Amazon)
今後も引き続き調べていきたいと思います。
その他参考
- SpringApplication (Spring Boot Docs 2.1.6.RELEASE API)
- SpringBootTest (Spring Boot Docs 2.1.6.RELEASE API)
- Spring Bootでコマンドラインアプリを作る時の注意点 - Qiita
- SpringBootでのJUnitでCommandLineRunnerが動くことの回避策 - Qiita
- spring - Prevent Application / CommandLineRunner classes from executing during JUnit testing - Stack Overflow