概要

テストが書かれていても、CI で不定期に落ちる、修正するたびに大量のテストが壊れる、何を検証しているのか読み取れない――こうした症状はテストコードのアンチパターンに起因していることが多いです。フレイキーテストは開発チームのテストへの信頼を損ない、やがて「赤くても無視する」文化を生みます。過剰なモックは実装の内部構造にテストを密結合させます。この記事では、実務で遭遇しやすい 6 つのアンチパターン――フレイキーテスト、テスト間の状態共有、過剰モック、テスト順序依存、sleep による待機、魔法の数値――の原因と改善後のコードを Before/After で対比します。

使いどころ

CI で不定期に失敗するフレイキーテストを特定し、時刻依存や共有状態の排除で安定化させる

レガシーテストの過剰モックやテスト順序依存を解消して保守性を改善する

テストコードレビューのチェックリストとしてチーム全体の品質を底上げする

sleep 依存の非同期テストを CountDownLatch や Awaitility に置き換えて再現性を上げる

バグ修正のたびに壊れる brittle test を減らし、リファクタリング耐性を上げる

コード例

TestAntipatternExamples.java
import org.junit.jupiter.api.*;
import java.time.*;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;

class TestAntipatternExamples {

    // === 1. 時刻依存の改善 ===
    @Nested @DisplayName("時刻依存")
    class TimeDependency {
        record DeadlineChecker(Clock clock) {
            boolean isOverdue(LocalDate deadline) { return LocalDate.now(clock).isAfter(deadline); }
        }
        @Test @DisplayName("Clock を固定して日付を制御する")
        void goodExample() {
            Clock fixed = Clock.fixed(Instant.parse("2025-04-01T00:00:00Z"), ZoneId.of("Asia/Tokyo"));
            assertTrue(new DeadlineChecker(fixed).isOverdue(LocalDate.of(2025, 3, 31)));
        }
    }

    // === 2. 状態共有の排除 ===
    @Nested @DisplayName("状態共有")
    class SharedState {
        static class Counter { int count; void inc() { count++; } void reset() { count = 0; } }
        private Counter counter;
        @BeforeEach void setUp() { counter = new Counter(); }

        @Test void once() { counter.inc(); assertEquals(1, counter.count); }
        @Test void twice() { counter.inc(); counter.inc(); assertEquals(2, counter.count); }
    }

    // === 3. 魔法の数値の排除 ===
    @Nested @DisplayName("魔法の数値")
    class MagicNumbers {
        record TaxCalc(double rate) { long tax(long price) { return Math.round(price * rate); } }
        @Test @DisplayName("定数で意図を明示")
        void good() {
            final double TAX_RATE = 0.10;
            assertEquals(100L, new TaxCalc(TAX_RATE).tax(1000L));
        }
    }

    // === 4. sleep の排除 ===
    @Nested @DisplayName("sleep 待機")
    class SleepWaiting {
        static class AsyncProc { String result; void run(CountDownLatch l) { new Thread(() -> { result = "done"; l.countDown(); }).start(); } }
        @Test @DisplayName("CountDownLatch で同期")
        void good() throws InterruptedException {
            var proc = new AsyncProc();
            var latch = new CountDownLatch(1);
            proc.run(latch);
            assertTrue(latch.await(5, TimeUnit.SECONDS));
            assertEquals("done", proc.result);
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でテストデータを定義すると意図が明確になる。テキストブロックで魔法の文字列を減らせる。

Java 17
// Java 17: Clock 注入 + テキストブロック
Clock fixedClock = Clock.fixed(
    Instant.parse("2025-03-31T00:00:00Z"), ZoneId.of("Asia/Tokyo"));
LocalDate today = LocalDate.now(fixedClock);
assertEquals(LocalDate.of(2025, 3, 31), today);

Library Comparison

JUnit 5 + Clock 注入時刻依存のテストで外部ライブラリなしに日時を固定できる。Clock を受け取る設計にプロダクションコードを変更する必要がある。
Awaitility非同期処理の完了をポーリングで待つテストを宣言的に記述できる。依存追加が必要。CountDownLatch で十分な場合はオーバースペック。
ArchUnit循環依存やレイヤー違反などアーキテクチャレベルのアンチパターンを検出したいとき。小規模プロジェクトでは費用対効果が見合わない場合がある。

注意点

Thread.sleep() をテストで使うと環境によってタイミングが変わりフレイキーになる。CountDownLatch 等の同期メカニズムに置き換える

static フィールドでテスト間の状態を共有すると実行順序依存になる。@BeforeEach で独立した状態を構築する

new Date() や LocalDate.now() を直接呼ぶとテストで日付を制御できない。Clock を注入する

アンチパターンの改善は一度にすべて行う必要はない。CI の失敗頻度が高いテストから優先的に修正する

失敗を再現できないまま ignore や retry に逃げると、根本原因が残り続ける。まず揺らぎの要因を見つけることが先

アンチパターンの指摘だけで終えるとチームに残らない。Before/After の具体例と置き換え先をセットで共有する方が定着しやすい

FAQ

フレイキーテストを特定する効率的な方法は。

CI の過去ログから失敗率の高いテストを集計するのが確実です。@RepeatedTest で再現性を確認する方法もあります。

過剰モックかどうかの判断基準は。

テスト1件あたりの when/verify が5個を超えたら見直しの目安です。テスト対象の設計を改善する方が根本的です。

テストの実行順序を固定すべきですか。

固定すべきではありません。各テストが独立して動くように設計するのが正しいアプローチです。

Thread.sleep を完全に禁止すべきですか。

原則として避けるべきです。外部システムの仕様上どうしても待機が必要な場合でも、できるだけ同期条件やタイムアウト制御に置き換える方が安定します。

アンチパターン改善はどこから手を付けるべきですか。

CI 失敗頻度が高いもの、修正時の巻き込みが大きいもの、チームが読みにくいと感じているものから着手するのが効果的です。全件を一度に直そうとしない方が継続できます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます