SQLAlchemyとfactoryboyその2

SQLAlchemyとFactoryBoyを使い、データベース操作を全てモックにしてテストを行う場合、`unittest.mock`を使用してSQLAlchemyのセッションやクエリをモック化し、`factory_boy`でテストデータの生成を行います。この方法では、実際のデータベースにアクセスせず、全ての操作がモックされるため、完全にスタンドアロンでのテストが可能です。

### 1. モデルクラスの定義

まず、通常のSQLAlchemyのモデルクラスを定義します。データベースは使用せず、後でモックを使います。

```python
# 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用機能は使用せず、単にオブジェクトを生成するためのファクトリーを作成します。

```python
# 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`オブジェクトやクエリの結果をモックします。

```python
# 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`を使用して操作が期待通り行われたかを確認します。

```python
# 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操作のテストを行うことができます。

```bash
pytest
```

### 各部分の説明

- **モックセッションの使用**:
  テスト中に実際のSQLAlchemyのセッションやデータベースに依存することなく、`mock.Mock()`を使ってセッションの操作をモックします。これにより、セッションが期待通りに動作したかどうかを確認することができます。
  
- **FactoryBoyでモックデータを生成**:
  `factory_boy`を使用して、テストで使用するモックデータを生成します。`FactoryBoy`は単純にモデルオブジェクトを生成するため、データベースに依存せず、モックデータを簡単に作成できます。

- **メソッドの呼び出し確認**:
  モックを使って、`session.add()`, `session.commit()`, `session.refresh()`などが期待通りに呼び出されたかを確認します。`mock.ANY`を使って、引数の内容には依存せずメソッドが呼ばれたかをチェックしています。

### まとめ

- **実際のデータベースは使わない**: このアプローチでは、全てのデータベース操作をモック化しており、実際のデータベースに依存せずにテストを実行できます。
- **FactoryBoyでデータ生成**: FactoryBoyを使用して、テスト用のデータを簡単に生成でき、モデルのテストに便利です。
- **モックの使用**: `unittest.mock`ライブラリを使用して、セッションやクエリ操作の挙動をシミュレートし、テストが独立して実行されるようにします。

これで、SQLAlchemyとFactoryBoyを使用し、データベース操作を全てモック化したテストを実行する準備ができました。