SQLAlchemyとFactoryBoyを使い、データベース操作を全てモックにしてテストを行う場合、unittest.mock
を使用してSQLAlchemyのセッションやクエリをモック化し、factory_boy
でテストデータの生成を行います。この方法では、実際のデータベースにアクセスせず、全ての操作がモックされるため、完全にスタンドアロンでのテストが可能です。
1. モデルクラスの定義
まず、通常のSQLAlchemyのモデルクラスを定義します。データベースは使用せず、後でモックを使います。
# models.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class MyModel(Base):
__tablename__ = 'my_table'
id = Column(Integer, primary_key=True, autoincrement=True)
text = Column(String, nullable=False)
int_field = Column(Integer, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<MyModel(id={self.id}, text='{self.text}', int_field={self.int_field}, created_at={self.created_at})>"
2. Factoryの定義
次に、factory_boy
を使用してモックデータを生成します。実際にはデータベース操作を行わないので、FactoryBoyのSQLAlchemy用機能は使用せず、単にオブジェクトを生成するためのファクトリーを作成します。
# factories.py
import factory
from models import MyModel
from datetime import datetime
class MyModelFactory(factory.Factory):
class Meta:
model = MyModel
id = factory.Sequence(lambda n: n + 1)
text = factory.Faker('sentence')
int_field = factory.Faker('random_int', min=1, max=100)
created_at = factory.LazyFunction(datetime.utcnow)
3. CRUD操作のモック
次に、SQLAlchemyのCRUD操作をモックするためのテスト用コードを用意します。unittest.mock
を使って、session
オブジェクトやクエリの結果をモックします。
# operations.py
def create_record(session, text, int_field):
"""新しいレコードを作成し、データベースに追加します。"""
new_record = MyModel(text=text, int_field=int_field)
session.add(new_record)
session.commit()
session.refresh(new_record)
return new_record
def update_record(session, record_id, new_text=None, new_int_field=None):
"""既存のレコードを更新します。"""
record = session.query(MyModel).filter(MyModel.id == record_id).first()
if not record:
return None
if new_text is not None:
record.text = new_text
if new_int_field is not None:
record.int_field = new_int_field
session.commit()
session.refresh(record)
return record
def get_record(session, record_id):
"""指定したIDのレコードを取得します。"""
return session.query(MyModel).filter(MyModel.id == record_id).first()
4. モックを使ったテストの実装
次に、テストではSQLAlchemyのセッションをモックします。実際のデータベースは使わず、mock
を使用して操作が期待通り行われたかを確認します。
# test_operations.py
import pytest
from unittest import mock
from factories import MyModelFactory
from operations import create_record, update_record, get_record
from models import MyModel
@pytest.fixture
def mock_session():
"""SQLAlchemyのセッションをモックします。"""
return mock.Mock()
def test_create_record(mock_session):
"""レコードの作成をモックでテストします。"""
# FactoryBoyでモックレコードを生成
new_record = MyModelFactory.build()
# モックセッションのadd, commit, refreshの動作を設定
mock_session.add.return_value = None
mock_session.commit.return_value = None
mock_session.refresh.return_value = None
# create_record関数をテスト
result = create_record(mock_session, text=new_record.text, int_field=new_record.int_field)
# add, commit, refreshが呼び出されたか確認
mock_session.add.assert_called_once_with(mock.ANY)
mock_session.commit.assert_called_once()
mock_session.refresh.assert_called_once_with(mock.ANY)
# 結果の確認
assert result.text == new_record.text
assert result.int_field == new_record.int_field
def test_update_record(mock_session):
"""レコードの更新をモックでテストします。"""
# 既存レコードのモックを作成
existing_record = MyModelFactory.build()
# モックセッションのクエリ動作を設定
mock_session.query.return_value.filter.return_value.first.return_value = existing_record
# レコードを更新
updated_record = update_record(mock_session, record_id=existing_record.id, new_text="Updated text", new_int_field=100)
# クエリが正しく呼ばれたか確認
mock_session.query.return_value.filter.return_value.first.assert_called_once()
# 結果の確認
assert updated_record.text == "Updated text"
assert updated_record.int_field == 100
def test_get_record(mock_session):
"""レコードの取得をモックでテストします。"""
# 既存レコードのモックを作成
existing_record = MyModelFactory.build()
# モックセッションのクエリ動作を設定
mock_session.query.return_value.filter.return_value.first.return_value = existing_record
# レコードを取得
fetched_record = get_record(mock_session, record_id=existing_record.id)
# クエリが正しく呼ばれたか確認
mock_session.query.return_value.filter.return_value.first.assert_called_once()
# 結果の確認
assert fetched_record.id == existing_record.id
assert fetched_record.text == existing_record.text
assert fetched_record.int_field == existing_record.int_field
5. テストの実行
このテストコードをpytest
で実行することで、実際にデータベースを使用せずにCRUD操作のテストを行うことができます。
pytest
各部分の説明
-
モックセッションの使用:
テスト中に実際のSQLAlchemyのセッションやデータベースに依存することなく、mock.Mock()
を使ってセッションの操作をモックします。これにより、セッションが期待通りに動作したかどうかを確認することができます。 -
FactoryBoyでモックデータを生成:
factory_boy
を使用して、テストで使用するモックデータを生成します。FactoryBoy
は単純にモデルオブジェクトを生成するため、データベースに依存せず、モックデータを簡単に作成できます。 -
メソッドの呼び出し確認:
モックを使って、session.add()
,session.commit()
,session.refresh()
などが期待通りに呼び出されたかを確認します。mock.ANY
を使って、引数の内容には依存せずメソッドが呼ばれたかをチェックしています。
まとめ
- 実際のデータベースは使わない: このアプローチでは、全てのデータベース操作をモック化しており、実際のデータベースに依存せずにテストを実行できます。
- FactoryBoyでデータ生成: FactoryBoyを使用して、テスト用のデータを簡単に生成でき、モデルのテストに便利です。
-
モックの使用:
unittest.mock
ライブラリを使用して、セッションやクエリ操作の挙動をシミュレートし、テストが独立して実行されるようにします。
これで、SQLAlchemyとFactoryBoyを使用し、データベース操作を全てモック化したテストを実行する準備ができました。