Testcontainers を使用したシフト レフト テスト: ローカル統合テストによるバグの早期発見

現代のソフトウェア開発ではスピードと俊敏性が重視されているため、効率的なテストが重要になっています。DORA の調査によると、エリート チームは高いパフォーマンスと信頼性の両方で繁栄することが明らかになっています。リードタイムを 127倍短縮、年間 182件のデプロイメント数、変更失敗率を 8倍、そして最も印象的なことに、インシデント後の復旧時間を 2倍293短縮することができます。秘密のソースは、彼らが「左にシフト」することです。 

シフトレフトは、テストやセキュリティなどの統合アクティビティを開発サイクルの早い段階に移行し、チームが本番環境に到達する前に問題を検出して修正できるようにするプラクティスです。ローカルテストと統合テストを早期に組み込むことで、開発者はコストのかかる後期段階の欠陥を防ぎ、開発を加速し、ソフトウェアの品質を向上させることができます。 

この記事では、統合テストが開発の内部ループの早い段階で欠陥を見つけるのにどのように役立つか、 また、Testcontainers が単体テストと同じくらい軽量で簡単に感じられるようにする方法を学びます。最後に、統合テストのシフトレフトが開発プロセスの速度と変更のリードタイムに与える影響を、DORA メトリクスに従って分析します。 

実際の例: ユーザー登録での大文字と小文字の区別のバグ

従来のワークフローでは、統合テストと E テスト2E テストは開発サイクルの外側のループで実行されることが多く、バグ検出の遅延や高価な修正につながります。たとえば、ユーザーがメールアドレスを入力するユーザー登録サービスを構築している場合は、メールが大文字と小文字を区別せず、保存時に重複しないようにする必要があります。 

大文字と小文字の区別が適切に処理されず、データベースによって管理されていると想定されている場合、大文字と小文字のみが異なる重複メールを使用してユーザーが登録できるシナリオのテストは、E テスト2E テストまたは手動チェック中にのみ発生します。その段階では、SDLCでは遅すぎ、コストのかかる修正につながる可能性があります。

テストを早期に移行し、開発者がデータベース、メッセージブローカー、クラウドエミュレーター、その他のマイクロサービスなどの実際のサービスをローカルでスピンアップできるようにすることで、テストプロセスが大幅に高速化されます。これにより、開発者は欠陥をより早く検出して解決でき、費用のかかる後期修正を防ぐことができます。

このシナリオ例と、さまざまなタイプのテストでそれがどのように処理されるかを深く掘り下げてみましょう。

シナリオ

新しい開発者がユーザー登録サービスを実装し、運用環境へのデプロイを準備しています。

registerUser メソッドのコード例

1
2
3
4
5
6
7
8
9
10
11
12
async registerUser(email: string, username: string): Promise<User> {
    const existingUser = await this.userRepository.findOne({
        where: {
            email: email         
        }
    });
 
    if (existingUser) {
        throw new Error("Email already exists");
    }
    ...
}

バグ

registerUser メソッドは大文字と小文字の区別を適切に処理せず、既定ではデータベースまたは UI フレームワークに依存して大文字と小文字の区別を区別しません。したがって、実際には、ユーザーは大文字と小文字の両方(例: user@example.comUSER@example.com)で重複する電子メールを登録できます。

インパクト

  • 認証の問題は、電子メールの大文字と小文字の不一致がログイン失敗を引き起こすために発生します。
  • セキュリティの脆弱性は、ユーザー ID の重複が原因で発生します。
  • データの不整合は、ユーザーID管理を複雑にします。

テスト方法 1: 単体テスト。 

これらのテストではコード自体のみが検証されるため、電子メールの大文字と小文字の区別の検証は、SQL クエリが実行されるデータベースに依存します。単体テストは実際のデータベースに対しては実行されないため、大文字と小文字の区別などの問題をキャッチできません。 

テスト方法 2:エンドツーエンドのテストまたは手動チェック。 

これらの検証では、コードがステージング環境にデプロイされた後にのみ問題がキャッチされます。自動化は役立ちますが、開発サイクルの後半で問題を検出すると、開発者へのフィードバックが遅れ、修正に時間とコストがかかります。

テスト方法 3: モックを使用して、ユニット テストとデータベースの対話をシミュレートします。 

うまくいき、迅速に反復できるアプローチの 1 つは、データベースレイヤーをモックし、エラーで応答するモックリポジトリを定義することです。次に、非常に高速に実行される単体テストを記述できます。

1
2
3
4
5
6
test('should prevent registration with same email in different case', async () => {
  const userService = new UserRegistrationService(new MockRepository());
  await userService.registerUser({ email: 'user@example.com', password: 'password123' });
  await expect(userService.registerUser({ email: 'USER@example.com', password: 'password123' }))
    .rejects.toThrow('Email already exists');
});

上記の例では、User サービスは、データベースのメモリ内表現 (つまり、ユーザーのマップ) を保持するモックリポジトリを使用して作成されます。このモックリポジトリは、ユーザーが 2 回渡したかどうかを検出し、おそらくユーザー名を大文字と小文字を区別しないキーとして使用し、予想されるエラーを返します。 

ここでは、検証ロジックをモックにコーディングし、Userサービスまたはデータベースが何をすべきかを複製する必要があります。ユーザーの検証に変更が必要な場合 (例:特殊文字を含まないため、モックも変更する必要があります。それ以外の場合、テストは検証の古い状態に対してアサートします。モックの使用がコードベース全体に分散している場合、このメンテナンスは非常に困難になる可能性があります。

これを避けるために、依存しているサービスの実際の表現を使用した統合テストを検討します。上記の例では、データベースリポジトリを使用すると、テスト対象に対する信頼性が向上するため、モックよりもはるかに優れています。

テスト手法 4: Testcontainersによるシフトレフトローカル統合テスト 

モックを使用したり、ステージングが統合テストや E テスト2E テストを実行するのを待ったりする代わりに、問題を早期に検出できます。これは、開発者が実際のPostgreSQLデータベースで Testcontainers を使用して、開発者の内部ループでプロジェクトの統合テストをローカルに実行できるようにすることで実現されます。

利点

  • 時間の節約:テストは数秒で実行され、バグを早期に発見します。
  • より現実的なテスト: モックの代わりに実際のデータベースを使用します。
  • 本番環境の準備状況の信頼性: ビジネスクリティカルなロジックが期待どおりに動作することを確認します。

統合テストの例

まず、Testcontainersライブラリを使用してPostgreSQLコンテナを設定し、このPostgreSQLインスタンスに接続するためのuserRepositoryを作成しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let userService: UserRegistrationService;
 
beforeAll(async () => {
        container = await new PostgreSqlContainer("postgres:16")
            .start();
         
        dataSource = new DataSource({
            type: "postgres",
            host: container.getHost(),
            port: container.getMappedPort(5432),
            username: container.getUsername(),
            password: container.getPassword(),
            database: container.getDatabase(),
            entities: [User],
            synchronize: true,
            logging: true,
            connectTimeoutMS: 5000
        });
        await dataSource.initialize();
        const userRepository = dataSource.getRepository(User);
        userService = new UserRegistrationService(userRepository);
}, 30000);

これで、初期化されたuserServiceを使用して、registerUserメソッドを使用して、実際のPostgreSQLインスタンスでのユーザー登録をテストできます。

1
2
3
4
5
test('should prevent registration with same email in different case', async () => {
  await userService.registerUser({ email: 'user@example.com', password: 'password123' });
  await expect(userService.registerUser({ email: 'USER@example.com', password: 'password123' }))
    .rejects.toThrow('Email already exists');
});

なぜこれが機能するのか

  • Testcontainersを介して実際のPostgreSQLデータベースを使用
  • 大文字と小文字を区別しない電子メールの一意性を検証
  • メールの保存形式を確認します

Testcontainersがどのように役立つか

Testcontainersモジュールは 、最も一般的なテクノロジーの事前設定された実装を提供し、堅牢なテストをこれまで以上に簡単に記述できます。アプリケーションがデータベース、メッセージブローカー、AWSなどのクラウドサービス(LocalStack経由)、またはその他のマイクロサービスに依存しているかどうかにかかわらず、Testcontainersにはテストワークフローを合理化するモジュールがあります。

Testcontainers を使用すると、サービスレベルのインタラクションをモックしてシミュレートしたり、コントラクトテストを使用してサービスが他のサービスとどのように相互作用するかを確認したりすることもできます。このアプローチを実際の依存関係に対するローカルテストと組み合わせることで、Testcontainersはローカル統合テストの包括的なソリューションを提供し、セットアップと管理が困難でコストがかかることが多い共有統合テスト環境の必要性を排除します。Testcontainers テストを実行するには、コンテナをスピンアップするための Docker コンテキストが必要です。Docker Desktop は、ローカル テスト用の Testcontainers とのシームレスな互換性を保証します。 

Testcontainers Cloud:ハイパフォーマンスチームのためのスケーラブルなテスト

Testcontainersは、実際の依存関係をローカルで使用して統合テストを可能にする優れたソリューションです。テストをさらに一歩進めて、チーム間でのTestcontainersの使用のスケーリング、テストに使用されるイメージの監視、CIでのTestcontainersテストのシームレスな実行を行いたい場合は、 Testcontainers Cloudの使用を検討する必要があります。これは、専用のテストインフラストラクチャを管理するオーバーヘッドのない一時的な環境を提供します。Testcontainers CloudをローカルおよびCIで使用すると、一貫したテスト結果が保証され、コード変更に対する信頼性が高まります。さらに、Testcontainers Cloudを使用すると、複数のパイプライン間でCIの統合テストをシームレスに実行できるため、高品質の基準を大規模に維持するのに役立ちます。最後に、Testcontainers Cloudはより安全で、コンテナのセキュリティメカニズムに対してより厳しい要件を持つチームや企業に最適です。   

シフトレフトテストのビジネスへの影響の測定

これまで見てきたように、Testcontainersを使用したシフトレフトテストは、欠陥検出率と時間を大幅に改善し、開発者のコンテキスト切り替えを減らします。上記の例を取り上げて、さまざまな本番環境のデプロイワークフローと、初期段階のテストが開発者の生産性にどのように影響するかを比較してみましょう。 

従来のワークフロー (共有統合環境)

プロセスの内訳:

従来のワークフローでは、フィーチャー・コードの記述、ローカルでのユニット・テストの実行、変更のコミット、および外部ループでの検証フローのプル・リクエストの作成が行われていました。外側のループでバグが検出された場合、開発者は IDE に戻り、ユニット テストをローカルで実行するプロセスと、修正を検証するための他の手順を繰り返す必要があります。 

シフト左なしのブログ

図 1: 従来の共有統合環境のワークフローは、各ステップにかかった時間別に分類されています。

変更のリードタイム(LTC): バグを検出して修正するには、少なくとも 1 時間から 2 時間かかります (CI/CD の負荷と確立されたプラクティスによって異なります)。最良のシナリオでは、コードのコミットから運用環境へのデプロイまで約 2 時間かかります。最悪のシナリオでは、複数回のイテレーションが必要な場合は、数時間または数日かかる場合があります。

デプロイ頻度 (DF) への影響: パイプラインの障害の修正には約 2 時間かかる場合があり、1 日の時間制約 (1 日の8時間労働時間) があるため、現実的には 1 日に 3 回から 4 回しかデプロイできません。複数の障害が発生すると、デプロイの頻度がさらに低下する可能性があります。

その他の関連費用: パイプライン ワーカーの実行時間 (分) と共有統合環境のメンテナンス コスト。

開発者コンテキストの切り替え: バグ検出はコードのコミットから約 30 分後に行われるため、開発者は集中力を失います。これにより、常にコンテキストの切り替え、デバッグ、そして再びコンテキストの切り替えを行う必要があるため、認知負荷が増加します。

シフトレフトワークフロー(Testcontainersを使用したローカル統合テスト)

プロセスの内訳:

シフトレフトのワークフローははるかに単純で、コードの記述と単体テストの実行から始まります。開発者は、外側のループで統合テストを実行する代わりに、内側のループでローカルに実行して、問題のトラブルシューティングと修正を行うことができます。変更は、次の手順と外側のループに進む前に再度確認されます。 

左シフトのブログ

図 2: Shift-Left と Testcontainers を使用したローカル統合テストのワークフローを、各ステップにかかった時間別に分類。フィードバックループははるかに高速になり、開発者の時間とダウンストリームの頭痛の種を節約できます。

変更のリードタイム(LTC): 開発者の内部ループのバグを検出して修正するのに 20 分もかかりません。したがって、ローカル統合テストでは、共有統合環境でのテストよりも少なくとも 65% 高速に障害を特定できます。  

デプロイ頻度 (DF) への影響: 欠陥が特定され、20分以内にローカルで修正されたため、パイプラインは本番環境まで実行され、毎日10つ以上のデプロイが可能になります。

追加の関連コスト: Testcontainers Cloud の5 分が消費されます。  

開発者コンテキストの切り替え: ローカルで実行されているテストは、コードの変更に関する即時のフィードバックを提供し、開発者はIDE内と内部ループに集中できるため、開発者はコンテキストを切り替える必要はありません。

重要なポイント

従来のワークフロー(共有統合環境)シフトレフトワークフロー(Testcontainersを使用したローカル統合テスト)改善点とその他の参考資料
変更のリードタイムの短縮(LTCコードの変更が数時間または数日で検証されます。開発者は、共有の CI/CD 環境を待ちます。コードの変更を数分で検証します。テストは即時かつローカルに行われます。>65% 変更のリードタイムの短縮 (LTC) Microsoft は、シフトレフト手法を採用することで、リードタイムを数日から数時間に短縮しました。
より高いデプロイメント頻度(DF)デプロイは、検証サイクルが遅いため、毎日、毎週、または毎月行われます。継続的なテストにより、1 日に複数のデプロイが可能になります。2x デプロイ頻度の増加 DORA レポート2024、シフトレフトのプラクティスがデプロイ頻度の 2 倍以上であることを示しています。エリートチームは 182倍頻繁にデプロイします。
変更失敗率(CFR)の低下本番環境に漏れるバグは、コストのかかるロールバックや緊急修正につながる可能性があります。CI/CD でより多くのバグが早期に検出され、運用エラーが減少します。変更失敗率の低下 – IBMのシステム科学研究所は 、生産で見つかった欠陥は、早期に発見された欠陥よりも修正コストが 15倍もかかると推定しています。
平均復旧時間(MTTR)の短縮修正には、共有環境での複雑なデバッグのために、数時間、数日、または数週間かかります。ローカルテストによる迅速なバグ解決。修正は数分で確認されました。MTTRの高速化—DORA のエリート パフォーマーは 、低パフォーマーが数週間から 1 か月かかるのに対し、1 時間未満でサービスを再開します。
コスト削減高価な共有環境、遅いパイプラインの実行、高いメンテナンスコスト。コストのかかるテスト環境を排除し、インフラストラクチャの費用を削減します。大幅なコスト削減ThoughtWorks Technology Radarは 、共有統合環境が脆弱で高価であることを強調しています。

表 1: Testcontainers を使用したローカル テストでシフト レフト ワークフローを使用した主要なメトリックの改善の概要


結論

シフトレフトテストは、問題を早期に発見し、デバッグの労力を減らし、システムの安定性を高め、開発者の生産性を全体的に向上させることで、ソフトウェアの品質を向上させます。これまで見てきたように、共有統合環境に依存する従来のワークフローは、非効率性をもたらし、変更のリードタイム、デプロイの遅延、頻繁なコンテキスト切り替えによる認知負荷の増加を引き起こします。対照的に、ローカル統合テストに Testcontainers を導入することで、開発者は次のことを実現できます。

  • フィードバックループの迅速 化 – バグは数分以内に特定および解決されるため、遅延を防ぐことができます。
  • より信頼性の高いアプリケーションの動作 – 現実的な環境でのテストにより、リリースの信頼性を確保できます。
  • 高価なステージング環境への依存を減らす – 共有インフラストラクチャを最小限に抑えることで、コストを削減し、CI/CDプロセスを効率化します。
  • 開発者のフロー状態の改善 – ローカルのテストシナリオを簡単に設定し、デバッグのために迅速に再実行することで、開発者はイノベーションに集中できます。

Testcontainersは、ローカルでテストし、高価な問題を早期に発見するための簡単で効率的な方法を提供します。チーム間で拡張するために、開発者は Docker Desktop と Testcontainers Cloud を使用して、専用のテスト インフラストラクチャを維持する複雑さなしに、ローカル、CI、または一時的な環境で単体テストと統合テストを実行することを検討できます。TestcontainersTestcontainers Cloudの詳細については、ドキュメントをご覧ください。 

参考文献