前作の記事 では、テストが1本もないレガシーコードに対して、仕様を追うのではなく現状の振る舞いを固定する「特性テスト(characterization test)」を次に触るファイルから書き始める手順を解説しました。

しかし、最初の1本を書き終えて意気揚々と2本目、3本目とテストを増やし始めた直後、多くのエンジニアが新たな壁に突き当たります。「テストを単体で動かすと通るのに、まとめて実行すると落ちる」「テストの実行順序を入れ替えると失敗する」「テストを実行するたびにローカルDBにゴミデータが溜まり、手動でクリーンアップしなければならない」という現象です。

この原因の正体は、テスト実行によって引き起こされるグローバル変数やデータベースの 副作用(Side Effect) にあります。テスト汚染(Test Pollution)とは、あるテストが実行時に残したグローバル状態やデータベースの変更が後続のテストに漏洩し、テスト結果を非決定的にしてしまう現象です1

本記事では、Perl のレガシーコードベースを題材に、この副作用を安全に隔離する3つの実践パターン(動的スコープによる状態リセット、トランザクションロールバック、インメモリDB / 一時MySQLへのSeam切り)をステップ・バイ・ステップで実装します。

全体構成と副作用の伝播メカニズム

なぜレガシーコードではテストが増えると壊れ始めるのでしょうか。その構造的な要因は、共有データベースやグローバル変数(パッケージ変数、環境変数、シングルトン)に対する「暗黙の依存と書き換え」にあります。

flowchart TD
    subgraph 汚染の伝播
        T1["テストA (注文作成)"] -->|INSERT・グローバル変数書換| Shared["共有DB / パッケージ変数"]
        Shared -->|変更された状態が残留| T2["テストB (注文一覧取得)"]
        T2 -->|期待値とズレて失敗| Fail["テスト失敗 (順序依存)"]
    end

    subgraph Seam による隔離
        T3["テストA"] -->|dynamic scope / rollback| Seam1["隔離された状態 (Seam)"]
        Seam1 -->|テスト終了時に完全破棄| Clean["元のクリーンな状態に復元"]
        Clean --> T4["テストB"]
        T4 -->|独立して実行| Pass["安定して成功 (並列実行可能)"]
    end

グローバル状態が散乱したレガシーコードでは、テストAが行った設定変更が後続のテストへ漏洩し、隠れた結合を生みます2。DBへのレコード挿入のような副作用も同様にそのまま残り、後続のテストBはその汚染された状態を想定外の入力として受け取ってしまいます。

これを解決するために、本チュートリアルでは以下のファイル構成でテストスイートを作成し、3つの隔離パターンを実装していきます。

t/
├── 01_global_state_reset.t       # 動的スコープによる状態リセット
├── 02_db_transaction_rollback.t   # トランザクションロールバックによるDB隔離
├── 03_db_seam_instance.t          # インメモリSQLite / Test::mysqld へのSeam切り
└── lib/
    ├── LegacyApp.pm               # レガシーWebアプリケーション本体
    └── TestHelper.pm              # テスト共通初期化・DBヘルパー

環境構築と依存モジュールのセットアップ

本チュートリアルで動作させる環境は Perl 5.20 以上を想定しています。テスト作成に必要なモジュールを cpanm でインストールします。

cpanm Test2::V0 Test::MockModule Test::mysqld DBD::SQLite

各モジュールの役割は以下の通りです。

  • Test2::V0: 現代的なPerlテストスイートの標準バンドル
  • Test::MockModule: パッケージサブルーチンの動的モック化ツール
  • Test::mysqld: テスト実行時に一時的な MySQL インスタンスを自動起動・破棄するモジュール
  • DBD::SQLite: インメモリデータベース接続用のドライバ

ステップ1: 動的スコープ(local)によるグローバル状態の一時リセット

最初のパターンは、Perlの強力な機能である local 演算子(動的スコープ)を活用したグローバル変数の状態リセットです。

目的

レガシーコードで頻出する「パッケージグローバル変数」「環境変数($ENV)」「グローバル関数」への依存を、テストブロックの実行中だけ一時的に書き換え、ブロックを抜けた瞬間に元の状態へ自動復元させます。

コード提示

use Test2::V0;
use Test::MockModule;

# レガシーなパッケージとグローバル変数の定義(模擬)
package LegacyApp::Config {
    our $DEBUG_MODE = 0;
    our $API_KEY    = 'production-key';
}

package LegacyApp::Service {
    sub fetch_external_rate {
        # 外部の有料APIを呼ぶ重い処理(模擬)
        die "Real API should not be called in test!";
    }

    sub calculate_discount {
        my ($price) = @_;
        if ($LegacyApp::Config::DEBUG_MODE) {
            return 0; # デバッグ時は割引なし
        }
        return $price * 0.1;
    }
}

package main;

# 1. パッケージ変数の動的スコープ書き換え
subtest 'パッケージ変数がテストブロック内だけで安全に変更される' => sub {
    is $LegacyApp::Config::DEBUG_MODE, 0, '初期状態は 0';

    {
        # local で動的スコープを一時的に変更
        local $LegacyApp::Config::DEBUG_MODE = 1;
        is $LegacyApp::Config::DEBUG_MODE, 1, 'ブロック内では 1 に上書きされている';
        is LegacyApp::Service::calculate_discount(1000), 0, 'デバッグモードの挙動になる';
    }

    # ブロックを抜けると自動的に元の値へ復元される
    is $LegacyApp::Config::DEBUG_MODE, 0, 'ブロックを抜けると元の 0 に復元される';
    is LegacyApp::Service::calculate_discount(1000), 100, '通常の割引計算に戻る';
};

# 2. 環境変数 $ENV の動的スコープ書き換え
subtest '環境変数がテスト終了後に元に戻る' => sub {
    $ENV{APP_ENV} = 'production';

    {
        local $ENV{APP_ENV} = 'test';
        is $ENV{APP_ENV}, 'test', 'ブロック内では test';
    }

    is $ENV{APP_ENV}, 'production', 'ブロック外では production に復元';
};

# 3. Test::MockModule によるサブルーチンの局所的モック化
subtest 'サブルーチンがスコープ終了時に自動アンモックされる' => sub {
    {
        my $mock = Test::MockModule->new('LegacyApp::Service');
        $mock->redefine('fetch_external_rate', sub { return 1.25 });

        is LegacyApp::Service::fetch_external_rate(), 1.25, 'モックされたレートが返る';
    }

    # $mock 変数がスコープを抜けて破棄されると自動的に元のサブルーチンに戻る
    like dies { LegacyApp::Service::fetch_external_rate() }, qr/Real API should not be called/, '元の例外送出処理に戻る';
};

done_testing;

実行結果 / ログ

prove -v t/01_global_state_reset.t
t/01_global_state_reset.t .. 
# Seeded srand with seed '20260826' from local date.
    # Subtest: パッケージ変数がテストブロック内だけで安全に変更される
        ok 1 - 初期状態は 0
        ok 2 - ブロック内では 1 に上書きされている
        ok 3 - デバッグモードの挙動になる
        ok 4 - ブロックを抜けると元の 0 に復元される
        ok 5 - 通常の割引計算に戻る
    ok 1 - パッケージ変数がテストブロック内だけで安全に変更される
    # Subtest: 環境変数がテスト終了後に元に戻る
        ok 1 - ブロック内では test
        ok 2 - ブロック外では production に復元
    ok 2 - 環境変数がテスト終了後に元に戻る
    # Subtest: サブルーチンがスコープ終了時に自動アンモックされる
        ok 1 - モックされたレートが返る
        ok 2 - 元の例外送出処理に戻る
    ok 3 - サブルーチンがスコープ終了時に自動アンモックされる
1..3
ok
All tests successful.
Files=1, Tests=3,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.04 cusr  0.01 csys =  0.06 CPU)
Result: PASS

重要箇所の解説

Perlの local 演算子は、レキシカル変数を宣言する my とは異なり、既存のパッケージ変数の値を一時的に退避させ、現在のブロックおよびそこから呼び出されるすべての関数呼び出しに対して新しい値を割り当てます(動的スコープ)3。ブロックを抜けると、Perlの内部コンテキストスタックによって自動的に元の値へ復元(Save and Restore)されます。

また、Test::MockModule が生成するモックオブジェクトは、レキシカルスコープが終了すると自動的に元のサブルーチン実装へ復元されます4。これにより、後続のテストへ影響を残さずに外部依存を無力化できます。

ステップ2: トランザクションロールバックによるDB副作用の安全な隔離

2つ目のパターンは、テストケースごとにデータベーストランザクションを開始し、テスト終了時に必ずロールバックを行う手法です。

目的

テスト実行中に発行される INSERTUPDATE の副作用を本番同等のDB上に一切残さず、高速かつクリーンな状態でテストを完了させます。

コード提示

use Test2::V0;
use DBI;

# テスト用DBハンドルの準備(SQLite インメモリを利用して実演)
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
    RaiseError => 1,
    AutoCommit => 1,
});

# スキーマの作成と初期データの投入
$dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, balance INTEGER)');
$dbh->do("INSERT INTO users (id, name, balance) VALUES (1, 'Alice', 1000)");

# トランザクションロールバックによるテスト実行ヘルパー
sub run_in_transaction {
    my ($dbh, $code) = @_;

    # トランザクション開始
    $dbh->begin_work;

    my $guard = bless { dbh => $dbh, active => 1 }, 'TxnGuard';
    
    # テストコードを実行(例外が発生しても確実にガードへ抜ける)
    my $res = eval { $code->(); 1 };
    my $err = $@;

    # 必ずロールバック
    $guard->rollback;

    if ($err) {
        die $err;
    }
}

package TxnGuard {
    sub rollback {
        my ($self) = @_;
        if ($self->{active}) {
            $self->{dbh}->rollback;
            $self->{active} = 0;
        }
    }
    sub DESTROY {
        my ($self) = @_;
        $self->rollback;
    }
}

package main;

subtest 'テスト1: ユーザーを追加して残高を更新するが、終了後に破棄される' => sub {
    run_in_transaction($dbh, sub {
        $dbh->do("INSERT INTO users (id, name, balance) VALUES (2, 'Bob', 500)");
        $dbh->do("UPDATE users SET balance = balance + 500 WHERE id = 1");

        my ($bob_count) = $dbh->selectrow_array("SELECT COUNT(*) FROM users WHERE id = 2");
        is $bob_count, 1, 'トランザクション内では Bob が存在する';

        my ($alice_balance) = $dbh->selectrow_array("SELECT balance FROM users WHERE id = 1");
        is $alice_balance, 1500, 'トランザクション内では Alice の残高が 1500';
    });

    # トランザクション外(テスト完了後)の検証
    my ($total_users) = $dbh->selectrow_array("SELECT COUNT(*) FROM users");
    is $total_users, 1, 'テスト終了後、Bob の追加はロールバックされている';

    my ($alice_balance) = $dbh->selectrow_array("SELECT balance FROM users WHERE id = 1");
    is $alice_balance, 1000, 'Alice の残高も元の 1000 に戻っている';
};

subtest 'テスト2: テスト1の影響を受けずに初期状態から検証できる' => sub {
    run_in_transaction($dbh, sub {
        my ($total_users) = $dbh->selectrow_array("SELECT COUNT(*) FROM users");
        is $total_users, 1, '先行テストのゴミデータがなく、1件のみ存在する';
    });
};

done_testing;

実行結果 / ログ

prove -v t/02_db_transaction_rollback.t
t/02_db_transaction_rollback.t .. 
# Seeded srand with seed '20260826' from local date.
    # Subtest: テスト1: ユーザーを追加して残高を更新するが、終了後に破棄される
        ok 1 - トランザクション内では Bob が存在する
        ok 2 - トランザクション内では Alice の残高が 1500
        ok 3 - テスト終了後、Bob の追加はロールバックされている
        ok 4 - Alice の残高も元の 1000 に戻っている
    ok 1 - テスト1: ユーザーを追加して残高を更新するが、終了後に破棄される
    # Subtest: テスト2: テスト1の影響を受けずに初期状態から検証できる
        ok 1 - 先行テストのゴミデータがなく、1件のみ存在する
    ok 2 - テスト2: テスト1の影響を受けずに初期状態から検証できる
1..2
ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.03 cusr  0.01 csys =  0.05 CPU)
Result: PASS

重要箇所の解説

Martin Fowler が『Evolutionary Database Design』で指摘している通り、テストのたびにテーブルを DELETETRUNCATE するアプローチは、トランザクションロールバック方式に比べてテスト全体の実行時間で明確に不利になります5

トランザクションロールバック方式を採用すると、データベースエンジン内部の undo ログによって瞬時に変更が巻き戻されるため、ディスクI/Oやロック待ちのオーバーヘッドを大幅に削減できます。DBIx::Class を採用している環境では、DBIx::Class::Storage::TxnScopeGuard を利用することで、ブロックを抜けた瞬間に自動ロールバックされる RAII パターンをより簡潔に記述できます6

ステップ3: インメモリDB(SQLite)と一時MySQL(Test::mysqld)へのSeam切り

3つ目のパターンは、テスト対象コードが参照するDB接続そのものを、テスト実行時のみ安全な隔離インスタンスへ差し替える Seam(接合部)設計です。

目的

Michael Feathers は Seam を「コードをその場所で編集せずに振る舞いを変更できる場所」と定義しました7。環境変数やファクトリメソッドに Seam を設けることで、ローカル開発DBを汚すことなく、インメモリDB(SQLite)やテスト専用の MySQL インスタンス(Test::mysqld)に接続先を切り替えます。

コード提示

use Test2::V0;
use DBI;
use Test::mysqld;

# レガシーなDB接続管理クラス(模擬)
package LegacyApp::DB {
    sub get_dbh {
        # 環境変数 DATABASE_DSN があればそれを優先し、無ければデフォルトDBに繋ぐ(Seam)
        my $dsn = $ENV{DATABASE_DSN} || 'dbi:mysql:dbname=production_db;host=127.0.0.1';
        return DBI->connect($dsn, $ENV{DATABASE_USER} || '', $ENV{DATABASE_PASS} || '', {
            RaiseError => 1,
            AutoCommit => 1,
        });
    }
}

package main;

# パターン3A: SQLite インメモリDBへの Seam 切り
subtest 'パターン3A: 環境変数 Seam を通じて SQLite インメモリDBへ接続' => sub {
    # 動的スコープで DSN をインメモリ SQLite に差し替える
    local $ENV{DATABASE_DSN} = 'dbi:SQLite:dbname=:memory:';

    my $dbh = LegacyApp::DB::get_dbh();
    ok $dbh, 'SQLite インメモリDBに接続成功';

    $dbh->do('CREATE TABLE items (id INT, title TEXT)');
    $dbh->do("INSERT INTO items VALUES (1, 'Book')");

    my ($title) = $dbh->selectrow_array("SELECT title FROM items WHERE id = 1");
    is $title, 'Book', 'インメモリDB上で正常に読み書きできる';
};

# パターン3B: Test::mysqld による一時 MySQL インスタンスの自動管理
subtest 'パターン3B: Test::mysqld を用いた本番同等 MySQL インスタンスの完全隔離' => sub {
    # mysqld のバイナリが存在する場合のみ実行
    my $mysqld = eval {
        Test::mysqld->new(
            my_cnf => {
                'skip-networking' => '', # ソケット通信で高速化
            }
        )
    };

    if (!$mysqld) {
        skip_all "Test::mysqld could not be started: " . ($Test::mysqld::errstr || $@);
    }

    # 一時インスタンスの DSN を環境変数 Seam に注入
    local $ENV{DATABASE_DSN} = $mysqld->dsn;

    my $dbh = LegacyApp::DB::get_dbh();
    ok $dbh, '一時 MySQL インスタンスに接続成功';

    $dbh->do('CREATE TABLE logs (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR(255))');
    $dbh->do("INSERT INTO logs (msg) VALUES ('test message')");

    my ($id) = $dbh->selectrow_array("SELECT id FROM logs WHERE msg = 'test message'");
    is $id, 1, 'MySQL 固有の AUTO_INCREMENT が正常に動作する';

    # $mysqld 変数がスコープを抜けると、mysqld プロセスが停止し一時ディレクトリごと自動消滅する
};

done_testing;

実行結果 / ログ

prove -v t/03_db_seam_instance.t
t/03_db_seam_instance.t .. 
# Seeded srand with seed '20260826' from local date.
    # Subtest: パターン3A: 環境変数 Seam を通じて SQLite インメモリDBへ接続
        ok 1 - SQLite インメモリDBに接続成功
        ok 2 - インメモリDB上で正常に読み書きできる
    ok 1 - パターン3A: 環境変数 Seam を通じて SQLite インメモリDBへ接続
    # Subtest: パターン3B: Test::mysqld を用いた本番同等 MySQL インスタンスの完全隔離
        ok 1 - 一時 MySQL インスタンスに接続成功
        ok 2 - MySQL 固有の AUTO_INCREMENT が正常に動作する
    ok 2 - パターン3B: Test::mysqld を用いた本番同等 MySQL インスタンスの完全隔離
1..2
ok
All tests successful.
Files=1, Tests=2,  1 wallclock secs ( 0.01 usr  0.00 sys +  0.08 cusr  0.03 csys =  0.12 CPU)
Result: PASS

重要箇所の解説

Test::mysqld は、OSの一時ディレクトリ内に一時的な mysqld プロセスを起動し、テスト終了時にプロセス停止とディレクトリ削除を自動的に行います8

SQLite インメモリ接続はミリ秒単位で起動し軽量ですが、MySQL 固有の構文(ON DUPLICATE KEY UPDATE、JSON関数、日付計算、ストレージエンジンの挙動差分など)が存在する場合に「テストは通るが本番で動かない」という誤検知(False Positive)を引き起こします。Test::mysqld を用いることで、完全な本番忠実度を保ちながら隔離を実現できます。

3つの副作用隔離パターンの比較と選択基準

レガシーシステムの副作用隔離には銀の弾丸は存在せず、テストの実行速度、本番環境との忠実度、アプリケーションの構造制約に応じて適切なパターンを選択する必要があります。

隔離パターン主な対象実行速度本番忠実度導入難易度主な制約・注意点推奨ユースケース
パターン1: 動的スコープ(local)グローバル変数、設定値、外部API極めて高速中(モック依存)低(コード改修最小)スレッドセーフティ、内部状態の複雑な結合パッケージ変数や外部連携が多い箇所の局所テスト
パターン2: トランザクションロールバック共有RDBMS(MySQL等)のCRUD高速高(本番同等DB)中(コネクション管理)ネストトランザクション、外部APIとの不整合単一DBへの読み書きが中心のエンドポイントテスト
パターン3A: インメモリDB(SQLite)単純なCRUD、スキーマ完結型極めて高速低〜中(方言差分)中(スキーマ互換性)MySQL固有構文・JSON型等の非互換単体・結合テストの高速フィードバック
パターン3B: 一時DB(Test::mysqld)複雑なSQL、ストアード、制約中速(起動オーバーヘッド)極めて高い中〜高(バイナリ依存)起動時間(数百ms〜数秒)、mysqldバイナリ必須CIパイプラインでの回帰テスト、高信頼性テスト

日常的なローカル開発と特性テスト初期段階では「動的スコープ+トランザクションロールバック」または「SQLite」で高速なサイクルを回し、CI環境や重要な回帰テストでは「Test::mysqld」を併用するハイブリッド戦略が最も堅牢です。

テストスイートの安定化検証と並列実行

隔離パターンを導入した後は、本当にテスト間の順序依存が解消されているかを継続的に検証する仕組みを導入します。

シャッフル実行による順序依存の検出

Perl のテストランナーである prove には、テストファイルの実行順序をランダム化する -s--shuffle)オプションが備わっています9

prove --shuffle t/

実行順序をランダム化することで、「テストAの後にテストBを実行した時だけ通る」といった潜在的な状態依存バグを即座にあぶり出すことができます。

失敗した実行順序の再現とデバッグ

シャッフル実行によって特定の順序で失敗が発生した場合、--state オプションを使ってその順序を完全に再現できます9

# 1. シャッフル実行し、実行状態を保存する
prove -b --state=save --shuffle t/

# 2. 直前に失敗した順序と同一の順序で再実行してデバッグする
prove -b --state=last t/

並列実行による独立性の検証

副作用が完全に隔離されていれば、テストスイートをマルチプロセスで並列実行しても競合が発生しません。

prove -j4 t/

Google の調査でも示されている通り、テストスイートの肥大化に伴う非決定性の混入を防ぐには、テスト間の独立性確保とプロセス分離が不可欠です10

本番運用に向けた考慮事項とトラブルシューティング

副作用隔離を実際のコードベースに適用する際、現場で必ず直面する落とし穴とその対処法を整理します。

1. ネストされたトランザクションの扱い

アプリケーションコード内部で明示的に COMMIT を呼び出す処理が含まれている場合、外側のテストトランザクションが確定されてしまい、ロールバックできなくなります。

  • 対処法: フレームワークや ORM のトランザクションネスト機能(DBIx::Classtxn_do など)を利用し、最外層のテストブロックでのみコミット/ロールバックを制御するか、MySQL の SAVEPOINT 構文を利用して擬似的にネストをサポートします。

2. 外部API・メール送信・非同期ジョブの副作用

データベースはロールバックできても、テスト中に外部の決済APIを叩いたり、メール送信キューへメッセージを積んだりした副作用は巻き戻りません。

  • 対処法: 外部通信を行うHTTPクライアントやジョブキュー送信関数に対して、必ずステップ1の Test::MockModule による動的モックを適用し、テストプロセス外への通信を遮断します。

3. DDL実行による暗黙のコミット

MySQL などの RDBMS では、CREATE TABLEALTER TABLETRUNCATE TABLE などの DDL(データ定義言語)文を実行した瞬間に、現在進行中のトランザクションが暗黙的にコミットされます。

  • 対処法: テストケース内で動的にテーブルを作成・変更するのではなく、テストスイート全体の初期化フェーズ(TestHelper.pm や事前マイグレーション)で一度だけスキーマを構築し、テストケース内では DML(INSERT/UPDATE/DELETE)のみを実行します。

よくある質問(FAQ)

Note

Q: テストでトランザクションロールバックを使う場合の最大の注意点は何ですか? A: アプリケーション内部で明示的な COMMIT が発行されたり、Savepoint 非対応の環境でトランザクションが二重ネストされた場合にロールバックが機能しなくなる点です。その場合は、モックによるDBアクセスの差し替えや Test::mysqld による一時インスタンスの完全破棄を検討してください。

Note

Q: SQLite のインメモリDBと Test::mysqld のどちらを選ぶべきですか? A: 実行速度を最優先し標準的なSQLで完結している場合は SQLite :memory: が適していますが、MySQL 固有の構文(ON DUPLICATE KEY UPDATE 等)や厳密なトランザクション分離レベルに依存している場合は Test::mysqld を選ぶべきです。

まとめ

本記事で解説した「グローバル状態とDB汚染の隔離」における重要ポイントは以下の通りです。

  • 特性テストの2本目を書いた瞬間に発生する不具合の多くは、テストコードではなくグローバル変数やDBの「テスト汚染」が原因である。
  • グローバル変数の汚染は、動的スコープ(Perlの local)とモジュールモックによるブロック単位の自動復元で局所化できる。
  • DBの副作用は、トランザクションロールバックとインメモリDB/一時DBインスタンス(Test::mysqld)へのSeam切りで安全に隔離できる。
  • テスト順序依存を恒常的に防ぐため、CIおよびローカル実行では prove --shuffle によるランダム実行を標準化する。

レガシーシステムの特性テスト導入、テスト自動化基盤の構築、安全なリファクタリング計画のご相談は、ぜひ コンタクトフォーム よりお問い合わせください。

Footnotes

  1. Eradicating Non-Determinism in Tests Martin Fowler、2011年。非決定性テスト(Flaky Tests)が開発者の信頼を損なう問題、テスト間の状態漏洩(State Leakage)と完全隔離の原則。

  2. Clean Code Talks - Global State and Singletons Miško Hevery、Google Testing Blog、2008年11月21日。グローバル状態とシングルトンがテスト間の予期せぬ結合(Spooky Action at a Distance)を生む弊害。

  3. perlsub - Temporary Values via local() Perl 5 公式ドキュメント。local 演算子による動的スコープ(Dynamic Scoping)とコンテキストスタックによる値の自動復元仕様。

  4. Test::MockModule CPAN 公式ドキュメント。サブルーチンの一時的モック化とレキシカルスコープ終了時の自動アンモック。

  5. Evolutionary Database Design Martin Fowler / Pramod Sadalage、2016年。Database Testing 節における Transactional Rollback によるテストクリーンアップの高速化とネスト制約。

  6. DBIx::Class::Storage::TxnScopeGuard CPAN 公式ドキュメント。RAII パターンによるスコープ終了時の自動ロールバック制御。

  7. Michael Feathers 著『Working Effectively with Legacy Code』(ISBN 0-13-117705-2、Addison-Wesley、2004年)。Seam(接合部)の定義「コードをその場所で編集せずに振る舞いを変更できる場所」。

  8. Test::mysqld Kazuho Oku / KARUPA / SONGMU、MetaCPAN。一時 MySQL インスタンスの自動起動・破棄、本番同等エンジンでのテスト隔離。

  9. prove - Run tests through a harness Perl 5 公式ドキュメント。prove --shuffle による実行順序ランダム化、--state=save / --state=last による失敗順序の再現機能。 2

  10. Flaky Tests at Google and How We Mitigate Them John Micco、Google Testing Blog、2016年5月18日。テスト規模の拡大に伴う非決定性の増加と、テスト順序ランダム化・プロセス分離による対策。