SpringBootTestでApplicationRunnerを起動させたくない

@SpringBootTest でテストを起動したら、ApplicationRunner の処理まで実行されてしまった。

アプリ起動時に一度だけ実行したい処理を ApplicationRunner で書いていると、これはわりと簡単に起きます。コンテキスト生成時に runner が動くので、テストで意図せず本番用の処理が走ってしまうのです。

この記事では、なぜそうなるのか、どう止めるのかを整理します。

検証用のサンプルプロジェクトは GitHub の java-snippets リポジトリ に置いてあります。記事中のコードとあわせて、@SpringBootTest で runner が動く挙動をそのまま確認できます。サンプルは GitHub Actions で Spring Boot 3.5.14 / 4.0.6 と Java 17 / 21 / 25 の組み合わせでテストしています。

先に結論

  • @SpringBootTest はアプリケーションコンテキストを本物に近い形で起動するので、ApplicationRunner も実行されます
  • runner に本体ロジックを書かず、実処理は別の Service に切り出すのが基本です
  • 実行有無は @ConditionalOnProperty@Profile で制御するのが扱いやすいです

なぜ起きるのか

Spring Boot の ApplicationRunner は、SpringApplication に含まれているときに実行される bean です。公式ドキュメントでも、ApplicationRunner は「SpringApplication の中にいるときに実行される bean」と説明されています。

また、@SpringBootTest はテスト用の ApplicationContextSpringApplication 経由で作ります。そのため、通常のアプリ起動と同じように runner が動きます。

つまり、これはバグというより「そういう仕組み」です。

まずやるべきこと

1. runner に本体ロジックを書かない

ApplicationRunner は起動のきっかけに徹して、実際の処理は別のクラスに逃がします。

@Component
public class ImportApplicationRunner implements ApplicationRunner {

    private final ImportService importService;

    public ImportApplicationRunner(ImportService importService) {
        this.importService = importService;
    }

    @Override
    public void run(ApplicationArguments args) {
        importService.execute();
    }
}
@Service
public class ImportService {

    public void execute() {
        // ここに本体ロジックを書く
    }
}

この形にしておくと、通常の単体テストは ImportService を直接テストできます。@SpringBootTest に依存しなくて済むので、runner の起動を気にしなくてよくなります。

2. 実行するかどうかを設定で切り替える

runner を常に bean 登録するのではなく、設定で有効化するようにします。

@ConditionalOnProperty を使うと、テストではプロパティを false にして止めやすくなります。

@Component
@ConditionalOnProperty(prefix = "startup.import", name = "enabled", havingValue = "true")
public class ImportApplicationRunner implements ApplicationRunner {

    private final ImportService importService;

    public ImportApplicationRunner(ImportService importService) {
        this.importService = importService;
    }

    @Override
    public void run(ApplicationArguments args) {
        importService.execute();
    }
}
startup:
  import:
    enabled: true

テストでは無効化します。

@SpringBootTest(properties = "startup.import.enabled=false")
class ImportApplicationTest {

    @Test
    void contextLoads() {
    }
}

この方法のよいところは、アプリ起動時の挙動がコードから読み取りやすいことです。「この runner はこのプロパティが true のときだけ動く」と明示できます。

3. 環境ごとに分けるなら @Profile

実行環境がはっきり分かれているなら @Profile も有効です。

@Component
@Profile("startup-import")
public class ImportApplicationRunner implements ApplicationRunner {

    private final ImportService importService;

    public ImportApplicationRunner(ImportService importService) {
        this.importService = importService;
    }

    @Override
    public void run(ApplicationArguments args) {
        importService.execute();
    }
}

テストでは startup-import プロファイルを有効にしなければ起動しません。

@SpringBootTest
@ActiveProfiles("test")
class ImportApplicationTest {

    @Test
    void contextLoads() {
    }
}

@Profile はシンプルですが、プロファイル運用が増えると少し管理が重くなります。個人的には、単純な有効/無効の切り替えなら @ConditionalOnProperty のほうが読みやすいことが多いです。

どうしても止めたいだけなら

設計をすぐ変えられない場合は、テスト側で runner を差し替える方法もあります。

たとえば @MockitoBean で runner 自体をモックに置き換えれば、実装コードの run() は呼ばれません。

@SpringBootTest
class ImportApplicationTest {

    @MockitoBean
    private ImportApplicationRunner importApplicationRunner;

    @Test
    void contextLoads() {
    }
}

ただしこれは応急処置です。runner 自体の設計が重いままだと、テストごとに同じ問題が再発しやすくなります。

まとめ

@SpringBootTestApplicationRunner が動くのは、Spring Boot の仕様に沿った挙動です。止めたいなら、まずは runner を薄くして、実処理を Service に分離するのが第一歩です。

そのうえで、実行制御は @ConditionalOnProperty@Profile で行うと、テストでも運用でも扱いやすくなります。

参考

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)