@SpringBootTestでCommandLineRunnerが走らないようにする


はじめに

Spring BootでCommandLineRunnerを使ってバッチアプリケーションを作成したとき、@SpringBootTestをつけたテストを走らせるとテスト動作前にCommandLineRunnerのrunメソッドが走るということが起きました。
この記事はこの原因解明と回避策を書きます。

前提

ライブラリ バージョン
Spring Boot 2.1.6.RELEASE

ソース

今回のソースコードです。
https://github.com/kawakawaryuryu/command-line-runner-sample

どんな実装をしたか

ざっくり以下のような実装をしました。

HogeApplication.java
@SpringBootApplication
public class SampleApplication {
    public static void main(String... args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
JobLauncher.java
// 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;
    }
}
application.properties
hoge.value=hoge

今回のテスト対象メソッドであるhogeメソッドは本来もっとちゃんとした処理を行っていますが、ここではあえて簡略化しています。

そしてテストクラスは以下になります。

JobLauncherTest.java
@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アノテーションを使った方法について書きます。
以下のような実装をして無事解決しました。

JobLauncherTest.java
@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");
    }
}
TestConfiguration.java
@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)

今後も引き続き調べていきたいと思います。

その他参考