@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 はテスト用の ApplicationContext を SpringApplication 経由で作ります。そのため、通常のアプリ起動と同じように 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 自体の設計が重いままだと、テストごとに同じ問題が再発しやすくなります。
まとめ
@SpringBootTest で ApplicationRunner が動くのは、Spring Boot の仕様に沿った挙動です。止めたいなら、まずは runner を薄くして、実処理を Service に分離するのが第一歩です。
そのうえで、実行制御は @ConditionalOnProperty か @Profile で行うと、テストでも運用でも扱いやすくなります。