バックエンドテストにおけるモック、スタブ、フェイクの効果的な戦略
Ethan Miller
Product Engineer · Leapcell

はじめに
バックエンド開発の複雑な世界では、アプリケーションの信頼性と堅牢性を確保することが最優先事項です。これは多くの場合、ビジネスロジックからデータベースのやり取り、外部API呼び出しまで、さまざまなコンポーネントを厳密にテストすることにつながります。しかし、依存するサービスが利用できない、セットアップに費用がかかる、または決定論的でない場合、通常の単体テストや統合テストは煩雑になったり、遅くなったり、あるいは不可能になったりすることがあります。ここで、テストダブル—特にモック、スタブ、フェイク—の戦略的な適用が非常に価値のあるものになります。制御された代替品で実際の依存関係を知的に置き換えることにより、真の単体分離を達成し、テスト実行を高速化し、予測可能なテスト環境を作成できます。この記事では、バックエンドテストプロセスを合理化するためのこれらの強力な手法の効果的な活用について掘り下げていきます。
コアコンセプトの説明
その適用について詳しく説明する前に、モック、スタブ、フェイクのそれぞれの役割を理解することが重要です。しばしば混同して使用されますが、テストの領域では異なる目的を果たします。
-
スタブ: スタブは、テスト中に実行されるメソッド呼び出しに対して事前定義された応答を保持するオブジェクトです。原則として、メソッド呼び出しに対して事前に用意された応答を提供し、テストが外部システムに依存しないようにします。スタブは主に、(テスト対象システム)SUTにデータを提供することに関心があります。アサーションはスタブ自体ではなく、SUTの動作に対して行います。
-
モック: モックは、より洗練されたタイプのテストダブルです。スタブと同様に、事前定義された値を返すことができます。しかし、モックは、それとのやり取りを検証することも可能にします。特定のメソッドが、何回、どのような引数で呼び出されたかをアサートできます。モックは、オブジェクト間のやり取りをテストし、コマンドの呼び出しを検証するために不可欠です。
-
フェイク: フェイクは、インターフェースまたはクラスの軽量な実装であり、テストによく使用されます。一部の動作は機能しますが、本番環境には適していません。一般的な例は、実際のデータベースサーバーの代わりにテストに使用されるインメモリデータベースです。フェイクは、実際のものの簡略化されたバージョンであっても、実際に何かを行います。
効果的な実装と応用
一般的なバックエンドシナリオ、つまりデータベースおよび外部電子メールサービスとやり取りするユーザーサービスに焦点を当てて、実践的な例でこれらの概念を説明しましょう。
ユーザーを作成し、ウェルカムメールを送信する必要があるPythonの簡単なUserService
を検討してください。
# user_service.py class User: def __init__(self, user_id, name, email): self.user_id = user_id self.name = name self.email = email class Database: def save_user(self, user): print(f"Saving user {user.name} to database...") # Simulates actual database interaction pass class EmailService: def send_email(self, recipient, subject, body): print(f"Sending email to {recipient} with subject '{subject}'...") # Simulates actual email sending pass class UserService: def __init__(self, db: Database, email_service: EmailService): self.db = db self.email_service = email_service def create_user(self, user_id, name, email): user = User(user_id, name, email) self.db.save_user(user) self.email_service.send_email(email, "Welcome!", f"Hello {name}, welcome to our service!") return user
データ制御のためのスタブの使用
create_user
をテストするとき、実際のデータベースや電子メール送信メカニズムに関係なく、UserService
がユーザーデータを正しく処理することを確認したいです。データベースのスタブは、制御された環境を提供できます。
# test_user_service_stub.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithStubs(unittest.TestCase): def test_create_user_saves_and_sends_welcome_email(self): # .save_userが実際のDBとやり取りしないことを保証するため、データベースをスタブ化します # スタブの場合、SUTが依存する戻り値を設定しないのであれば、MagicMockをそのまま使用できます。 mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) # ここではスタブとして機能します。まだ呼び出しを検証しません。 user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe") # スタブテストでは、通常、スタブ自体のやり取りを検証しません。 # しかし、デモンストレーションのために、MagicMockをスタブとして使用する方法を示します。 # 通常はUserServiceの戻り値や内部状態(もしあれば)を検証します。 # mock_dbとmock_email_serviceを純粋にスタブとして扱う場合、 # 主要な関心事はUserServiceの動作になります。 # create_userがUserオブジェクトを返すという事実が、私たちの主要なアサーションです。
このスタブの例では、MagicMock(spec=Database)
はmock_db
がDatabase
オブジェクトのように振る舞うことを保証しますが、実際にはデータベースに接続しません。主にUserService
の戻り値(user
オブジェクト)に焦点を当てており、mock_db
とmock_email_service
は複雑な動作なしで依存関係を満たすスタブとして機能します。
対話検証のためのモックの使用
次に、モックを使用して、データベースのsave_user
と電子メールサービスのsend_email
が実際には正しい引数で呼び出されたことを検証しましょう。
# test_user_service_mock.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithMocks(unittest.TestCase): def test_create_user_interacts_with_dependencies_correctly(self): mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") # モックとのやり取りに関するアサーション: mock_db.save_user.assert_called_once_with(user) mock_email_service.send_email.assert_called_once_with( "john@example.com", "Welcome!", "Hello John Doe, welcome to our service!" ) self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe")
ここでは、mock_db
とmock_email_service
がモックとして使用されています。それらのメソッド(save_user
、send_email
)はUserService
が消費するもの何も返さないため、戻り値は設定していません。主な違いはassert_called_once_with
メソッドであり、これによりやり取りとそれらに渡された引数を明示的に検証できます。これは、UserService
が依存関係への呼び出しを正しく調整していることを確認するために重要です。
単純化された動作のためのフェイクの使用
Database
クラスにさらに複雑なfind_user
メソッドがあり、データベースから読み取るUserService
メソッドをテストしたいと想像してください。フェイクデータベースは、シンプルなインメモリ実装を提供できます。
# user_service.py (追加メソッド) # ... (既存のクラス) class UserService: # ... (既存の__init__とcreate_user) def get_user_by_id(self, user_id): return self.db.find_user(user_id)
# test_user_service_fake.py import unittest from user_service import UserService, User, Database, EmailService # Fake Database 実装 class FakeDatabase(Database): def __init__(self): self.users = {} # インメモリストレージ def save_user(self, user): self.users[user.user_id] = user print(f"Fake DB: Saved user {user.name}") def find_user(self, user_id): print(f"Fake DB: Finding user {user_id}") return self.users.get(user_id) class TestUserServiceWithFakes(unittest.TestCase): def test_get_user_by_id_retrieves_user_from_fake_db(self): fake_db = FakeDatabase() mock_email_service = MagicMock(spec=EmailService) # Email serviceは引き続きmock/stubで可 user_service = UserService(fake_db, mock_email_service) # まず、FakeDatabaseのsave_userを使用してユーザーを作成します user_service.create_user("456", "Jane Doe", "jane@example.com") # 次に、FakeDatabaseに保存されたデータを使用して、get_user_by_idメソッドをテストします retrieved_user = user_service.get_user_by_id("456") self.assertIsNotNone(retrieved_user) self.assertEqual(retrieved_user.name, "Jane Doe") self.assertEqual(retrieved_user.email, "jane@example.com") self.assertEqual(retrieved_user.user_id, "456")
このシナリオでは、FakeDatabase
はフェイクです。Database
インターフェースを実装していますが、実際のデータベース接続ではなく、インメモリ辞書を使用してユーザーを格納します。これにより、実際のデータベース接続のオーバーヘッドや複雑さを伴わずに、単純化されたデータベースのような動作を含むget_user_by_id
をテストできます。フェイクは、依然として高速で制御された状態を必要とする統合テストに最適です。
結論
モック、スタブ、フェイクの効果的な使用は、堅牢なバックエンドテストの基盤です。スタブは単純化されたデータを提供し、モックはやり取りを検証し、フェイクは軽量で動作する実装を提供し、それぞれがテストの分離と効率の実現において独自の役割を果たします。これらのテストダブルを習得することで、開発者はコードに自信を持つことができ、各ユニットが制御され予測可能な条件下で信頼性高くその責務を果たすことを知ることができます。これらのツールは、バックエンドコンポーネントの迅速で信頼性の高い検証を可能にします。