mcpField
U
unit-test-scheduled-async

unit-test-scheduled-async

github.com/giuseppe-trisciuoglioテスト・品質3653.7k 回インストール7 週間前に更新
リポジトリ

unit-test-scheduled-async について

Spring と JUnit 5 を使う開発者向けに、`@Scheduled` と `@Async` メソッドを分離してテストする方法を紹介します。実際のスケジュールを待たずに、非同期の結果や例外を検証できます。

主な機能

  1. 01メソッドを直接呼び出す
  2. 02CompletableFuture の結果確認
  3. 03Awaitility による条件待機
  4. 04非同期例外伝播のテスト
  5. 05タスク実行回数の検証

活用シーン

  1. 01スケジュールタスクのロジックテスト
  2. 02非同期メソッドの結果確認
  3. 03非同期エラー処理の検証
  4. 04待機なしでの cron ロジックテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-scheduled-async

インストール後、Claude Code で/unit-test-scheduled-asyncを入力して読み込まれたか確認します。

同梱ファイルreferences/examples.md

詳細

@Scheduled および @Async メソッドのユニットテスト

概要

JUnit 5 で Spring の @Scheduled および @Async メソッドをユニットテストするためのパターンです。CompletableFuture の結果をテストし、Awaitility を使って競合状態に対処し、スケジュールされたタスクの実行をモックし、実際のスケジュール間隔を待たずにエラー処理を検証します。

使用する場面

  • @Scheduled メソッドのロジックをテストする場合
  • @Async メソッドの動作をテストする場合
  • CompletableFuture の結果を検証する場合
  • 非同期エラー処理をテストする場合
  • 実際のスケジュール実行を待たずに cron 式のロジックをテストする場合
  • スレッドプールの動作と実行回数を検証する場合
  • バックグラウンドタスクのロジックを分離してテストする場合

手順

  1. @Async メソッドを直接呼び出します — Spring の非同期プロキシをバイパスします。ユニットテストではアノテーションは関係ありません
  2. 依存関係をモックします — @Mock と @InjectMocks(Mockito)を使用します
  3. 完了を待ちます — CompletableFuture.get(timeout, unit) または await().atMost(...).untilAsserted(...) を使用します
  4. @Scheduled メソッドを直接呼び出します — cron や fixedRate の実行を待たないでください。ユニットテストではアノテーションは無視されます
  5. 例外経路をテストします — CompletableFuture.get() で ExecutionException にラップされることを検証します

検証チェックポイント:

  • CompletableFuture.get() の後、モックの呼び出しを検証する前に、戻り値をアサートします
  • ExecutionException がスローされた場合は、.getCause() を確認して根本原因の例外を特定します
  • Awaitility がタイムアウトした場合は、条件が満たされるまで atMost() の時間を延ばすか、pollInterval() を短くします
  • タスクを複数回実行した後、verify() を呼び出す前に実行回数をアサートします

例

主なパターン — 完全な例は references/examples.md を参照してください。

// @Async: call directly, wait with CompletableFuture.get(timeout, unit)
@Service
class EmailService {
  @Async
  public CompletableFuture<Boolean> sendEmailAsync(String to) {
    return CompletableFuture.supplyAsync(() -> true);
  }
}
@Test
void shouldReturnCompletedFuture() throws Exception {
  EmailService service = new EmailService();
  Boolean result = service.sendEmailAsync("[email protected]").get(5, TimeUnit.SECONDS);
  assertThat(result).isTrue();
}

// @Scheduled: call directly, mock the repository
@Component
class DataRefreshTask {
  @InjectMocks private DataRepository dataRepository;
  @Scheduled(fixedDelay = 60000) public void refreshCache() { /* ... */ }
}
@Test
void shouldRefreshCache() {
  when(dataRepository.findAll()).thenReturn(List.of(new Data(1L, "item1")));
  dataRefreshTask.refreshCache();
  verify(dataRepository).findAll();
}

// Awaitility: use for race conditions with shared mutable state
@Test
void shouldProcessAllItems() {
  BackgroundWorker worker = new BackgroundWorker();
  worker.processItems(List.of("item1", "item2", "item3"));
  Awaitility.await()
    .atMost(Duration.ofSeconds(5))
    .pollInterval(Duration.ofMillis(100))
    .untilAsserted(() -> assertThat(worker.getProcessedCount()).isEqualTo(3));
}

// Mocked dependencies with exception handling
@Test
void shouldHandleAsyncExceptionGracefully() {
  doThrow(new RuntimeException("Email failed")).when(emailService).send(any());
  CompletableFuture<String> result = service.notifyUserAsync("user123");
  assertThatThrownBy(result::get)
    .isInstanceOf(ExecutionException.class)
    .hasCauseInstanceOf(RuntimeException.class);
}

Maven/Gradle の依存関係一式、追加のテストクラス、実行回数のパターンについては、references/examples.md を参照してください。

ベストプラクティス

  • テストが停止しないよう、CompletableFuture.get() には必ずタイムアウトを設定します
  • すべての依存関係をモックします — ユニットテストで実際の外部サービスを呼び出さないでください
  • Awaitility は競合状態に対処する場合にのみ使用し、単純な非同期メソッドでは直接呼び出しを優先します
  • @Scheduled のロジックを直接テストします — ユニットテストではアノテーションは無視されます
  • モックの呼び出しを検証する前に値をアサートし、非同期処理の完了後に検証します

よくある落とし穴

  • メソッドを直接呼び出さず、Spring の非同期エグゼキューターに依存する
  • CompletableFuture.get() にタイムアウトを設定し忘れる
  • 非同期メソッドでの例外の伝播をテストし忘れる
  • 非同期メソッドが内部で呼び出す依存関係をモックしない
  • ロジックを分離してテストせず、実際の cron や fixedRate のタイミングを待つ

制約と警告

  • @Async の自己呼び出し:同じクラス内の別のメソッドから @Async を呼び出すと、同期的に実行されます。Spring のプロキシがバイパスされるためです
  • スレッドプールの実行順序:ThreadPoolTaskScheduler は実行順序を保証しません
  • CompletableFuture のチェーン:中間段階で発生した例外が黙って失われる場合があります。各段階をテストしてください
  • Awaitility のタイムアウト:常に妥当な atMost() を設定してください。無期限の待機はテストスイートを停止させます
  • 実際のスケジューリングは行われません:ユニットテストでは @Scheduled は無視されます。メソッドを直接呼び出してください

参考資料

コメント