factory_boyを徹底解説!テストデータ生成を効率化するPythonライブラリ ✨

Python

はじめに:テストデータ作成の悩みとfactory_boy

ソフトウェア開発、特にWebアプリケーション開発において、テストは品質を担保する上で不可欠なプロセスです。そして、効果的なテストを実行するためには、質の高いテストデータが大量に必要となる場面が多くあります。しかし、このテストデータの作成は、しばしば開発者を悩ませる種となります 🤔。

手作業で一つ一つデータを作成するのは非効率的であり、特に複雑なデータ構造やリレーションシップを持つモデルの場合、その手間は膨大になります。また、静的なフィクスチャファイル(JSONやYAMLなど)を利用する方法もありますが、データの変更や追加が必要になった際のメンテナンス性が低いという課題がありました。

そこで登場するのが、factory_boyというPythonライブラリです! 🚀 factory_boyは、Ruby on Railsコミュニティで人気を博したfactory_bot(旧factory_girl)にインスパイアされたライブラリで、テスト用のオブジェクト(モデルインスタンスなど)を効率的かつ柔軟に生成するための強力なツールです。

factory_boyを使うことで、以下のようなメリットが得られます。

  • 効率化: 宣言的な構文で、少ないコードでテストデータを生成できます。
  • 可読性向上: どのようなデータが生成されるのかが分かりやすくなります。
  • 保守性向上: モデルの変更に追従しやすく、テストデータのメンテナンスが容易になります。
  • 柔軟性: 固定値だけでなく、動的な値や関連オブジェクトも簡単に生成できます。

この記事では、factory_boyの基本的な使い方から高度な機能、そして実践的なテクニックまで、詳細に解説していきます。factory_boyをマスターして、テストデータ作成の悩みから解放されましょう! 💪

基本的な使い方 🛠️

インストール

factory_boyのインストールはpipを使って簡単に行えます。

pip install factory-boy

factory_boy自体は標準のPythonライブラリ以外に依存関係はありませんが、Fakerなどの関連ライブラリを一緒に使うことが多いです(後述)。

基本的なファクトリ定義

factory_boyの中心となるのが「ファクトリ」クラスです。これは、特定のクラス(通常はモデルクラス)のインスタンスを生成するための設計図のようなものです。ファクトリはfactory.Factoryを継承して定義します。

最も基本的なファクトリ定義は以下のようになります。Metaクラス内にmodel属性で生成対象のクラスを指定し、クラス変数として各属性のデフォルト値を定義します。

import factory

# 例として、シンプルなUserクラスを定義
class User:
    def __init__(self, id, username, email, is_active=True):
        self.id = id
        self.username = username
        self.email = email
        self.is_active = is_active

    def __repr__(self):
        return f"<User: {self.username}>"

# Userクラスに対応するファクトリを定義
class UserFactory(factory.Factory):
    class Meta:
        model = User # 生成するモデルクラスを指定

    # 各属性のデフォルト値を設定
    id = factory.Sequence(lambda n: n + 1) # シーケンスを使って一意なIDを生成
    username = factory.Faker('user_name') # Fakerを使ってランダムなユーザー名を生成
    email = factory.LazyAttribute(lambda o: f'{o.username}@example.com') # LazyAttributeで他の属性値を利用
    is_active = True

上記の例では、いくつかのfactory_boyの機能を使っています。

  • factory.Sequence: 呼び出されるたびに連番を生成します。ユニーク制約のあるフィールドなどに便利です。
  • factory.Faker: Fakerライブラリと連携し、リアルなダミーデータ(名前、メールアドレス、住所など)を生成します。pip install Fakerで別途インストールが必要です。
  • factory.LazyAttribute: インスタンス生成時に動的に値を計算します。他の属性値(この例ではusername)を利用することも可能です。oは生成中のオブジェクトインスタンスを指します。

ファクトリを定義する際には、factories.pyのような専用のモジュールを作成するのが一般的です。

モデルとの連携 (ORMサポート)

factory_boyは、Django, SQLAlchemy, MongoEngineなどの主要なORMとスムーズに連携するための専用ファクトリクラスを提供しています。これにより、データベースへの保存などを簡単に行うことができます。

ORM対応するファクトリクラス主な機能
Djangofactory.django.DjangoModelFactorycreate()Model.objects.create()を呼び出し、DBに保存。
モデル指定時に'app_label.ModelName'形式も利用可能。
django_get_or_createオプションでget_or_create()を利用可能。
SQLAlchemyfactory.alchemy.SQLAlchemyModelFactorycreate()session.add()session.flush()/session.commit()を実行。
Meta.sqlalchemy_session で利用するセッションを指定。
MongoEnginefactory.mongoengine.MongoEngineFactorycreate()document.save()を実行。

例えば、Djangoモデルに対応するファクトリは以下のように定義します。

# models.py (Django)
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published_at = models.DateTimeField(null=True, blank=True)
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)

# factories.py
import factory
from django.contrib.auth.models import User
from .models import Article
import datetime
from django.utils import timezone

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
        django_get_or_create = ('username',) # 同じusernameのユーザーが既に存在すれば取得、なければ作成

    username = factory.Sequence(lambda n: f'user{n}')
    email = factory.LazyAttribute(lambda o: f'{o.username}@example.com')
    password = factory.PostGenerationMethodCall('set_password', 'defaultpassword') # パスワード設定

class ArticleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Faker('sentence', nb_words=6, locale='ja_JP') # 日本語の文を生成
    content = factory.Faker('text', max_nb_chars=1000, locale='ja_JP') # 日本語のテキストを生成
    published_at = factory.Faker('date_time_this_decade', tzinfo=timezone.get_current_timezone())
    author = factory.SubFactory(UserFactory) # 関連するUserも自動生成

この例ではさらにいくつかの機能を使用しています。

  • Meta.django_get_or_create: 指定されたフィールド(この場合はusername)でget_or_createを実行します。ユニークなフィールドを持つモデルで便利です。
  • factory.PostGenerationMethodCall: オブジェクトが生成された後(DB保存後)に呼び出すメソッドを指定します。DjangoのUserモデルのパスワード設定などに使えます。
  • factory.SubFactory: 関連するモデル(この場合はauthor)のファクトリを指定し、自動的に関連オブジェクトも生成・設定します。
  • Fakerlocale='ja_JP': Fakerに日本語のデータを生成させることができます。

インスタンス生成戦略:`build()`, `create()`, `stub()`

定義したファクトリを使ってインスタンスを生成するには、いくつかのメソッド(戦略)があります。

メソッド動作主な用途
build(**kwargs)オブジェクトのインスタンスを生成するが、データベースには保存しないモデルのメソッド単体のテストなど、データベースアクセスが不要な場合。
create(**kwargs)オブジェクトのインスタンスを生成し、データベースに保存する(ORM連携ファクトリの場合)。データベースとのインタラクションを含むテスト(ビューのテスト、統合テストなど)。
stub(**kwargs)オブジェクトのように振る舞うスタブ(単純なオブジェクト、通常はSimpleNamespace)を生成する。データベースには保存しない非常に高速なテストが必要で、オブジェクトの具体的な型やメソッドが重要でない場合。
attributes(**kwargs)オブジェクトを生成せず、属性とその値の辞書を返す。APIリクエストのペイロード作成など。
build_batch(size, **kwargs)指定した数のインスタンスをbuild戦略で生成し、リストで返す。複数の非永続オブジェクトが必要な場合。
create_batch(size, **kwargs)指定した数のインスタンスをcreate戦略で生成し、リストで返す。複数の永続オブジェクトが必要な場合。
stub_batch(size, **kwargs)指定した数のスタブをstub戦略で生成し、リストで返す。複数のスタブオブジェクトが必要な場合。

これらのメソッドには、ファクトリで定義されたデフォルト値を上書きするためのキーワード引数を渡すことができます。

# ArticleFactoryを使用する例 (Django)

# DBに保存しないArticleインスタンスを生成
article_built = ArticleFactory.build(title="下書きの記事")
print(article_built.title) # => 下書きの記事
print(article_built.pk)    # => None (DBに保存されていない)

# DBに保存されるArticleインスタンスを生成 (authorも自動生成・保存される)
article_created = ArticleFactory.create(published_at=None)
print(article_created.title) # => (ランダムなタイトル)
print(article_created.pk)    # => (DBで採番されたID)
print(article_created.published_at) # => None
print(article_created.author.username) # => userX (自動生成されたユーザー名)

# 属性の辞書を取得
attrs = ArticleFactory.attributes(title="API用タイトル")
print(attrs) # => {'title': 'API用タイトル', 'content': '...', ...}

# 複数のインスタンスを生成してDBに保存
articles = ArticleFactory.create_batch(5, published_at=timezone.now())
print(len(articles)) # => 5
print(articles[0].published_at) # => (現在時刻)

注意: create()戦略はデータベースへの書き込みを行うため、build()stub()に比べてテストの実行速度が遅くなる可能性があります。テストの目的に応じて適切な戦略を選択することが重要です。

属性の指定 (固定値、動的な値)

ファクトリの属性には、様々な方法で値を指定できます。

  • 固定値: attr_name = "固定値" のように直接値を指定します。
  • シーケンス (factory.Sequence): attr_name = factory.Sequence(lambda n: f"item_{n}") のように、呼び出されるたびにインクリメントされる数値nを使って値を生成します。
  • Faker (factory.Faker): attr_name = factory.Faker('provider_name', **kwargs) のように、Fakerライブラリを使ってランダムなデータを生成します。
  • 遅延属性 (factory.LazyAttribute): attr_name = factory.LazyAttribute(lambda obj: f"{obj.first_name}_{obj.last_name}") のように、インスタンス生成時に他の属性値objを使って動的に値を計算します。
  • 遅延関数 (factory.LazyFunction): attr_name = factory.LazyFunction(datetime.datetime.now) のように、インスタンス生成時に関数を呼び出してその戻り値を属性値とします。引数なしの関数のみサポートしています。
  • サブファクトリ (factory.SubFactory): related_obj = factory.SubFactory(RelatedFactory) のように、関連するオブジェクトを別のファクトリを使って生成します。

高度な機能 🚀

シーケンス (`factory.Sequence`) の詳細

factory.Sequenceは、一意な値を生成するのに非常に便利です。特にユニーク制約のあるフィールド(ユーザー名、メールアドレス、IDなど)に適しています。

class ProductFactory(factory.Factory):
    class Meta:
        model = Product # Productモデルがあると仮定

    sku = factory.Sequence(lambda n: f'PROD-{n:04d}') # PROD-0000, PROD-0001, ...
    name = factory.LazyAttribute(lambda o: f"製品 {o.sku}")

シーケンスのカウンターは、ファクトリクラスごとに管理されます。カウンターの初期値を設定したり、リセットしたりすることも可能です。

# カウンターの初期値を1000に設定
ProductFactory.reset_sequence(1000)
product1 = ProductFactory() # sku => PROD-1000
product2 = ProductFactory() # sku => PROD-1001

# 現在のシーケンス値を取得
current_seq = ProductFactory.get_sequence()
print(current_seq) # => 1002 (次に使われる値)

# カウンターをリセット (デフォルトは0から)
ProductFactory.reset_sequence()
product3 = ProductFactory() # sku => PROD-0000

遅延属性 (`factory.LazyAttribute`, `factory.LazyFunction`) の活用

factory.LazyAttribute は、他の属性値に基づいて値を動的に生成したい場合に強力です。引数として渡されるオブジェクト(通常oobjと命名)は、生成中のインスタンス自身を指します。

import random

class OrderFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Order # Orderモデル (user, amount, tax を持つと仮定)

    user = factory.SubFactory(UserFactory)
    amount = factory.Faker('pydecimal', left_digits=5, right_digits=2, positive=True)
    # 税額を金額に基づいて計算 (8%とする)
    tax = factory.LazyAttribute(lambda o: (o.amount * 8) // 100)
    # 注文日はランダムな過去の日付
    order_date = factory.LazyFunction(lambda: timezone.now() - datetime.timedelta(days=random.randint(1, 365)))

factory.LazyFunction は、引数を取らない単純な関数やメソッド呼び出しの結果を使いたい場合に便利です。

関連 (`factory.SubFactory`, `factory.RelatedFactory`)

モデル間のリレーションシップを表現するために、factory_boyはfactory.SubFactoryfactory.RelatedFactoryを提供します。

  • factory.SubFactory: 一対一(OneToOneField)や多対一(ForeignKey)のリレーションシップで使用します。親オブジェクトのファクトリ内で、関連オブジェクトのファクトリを指定します。create戦略の場合、SubFactoryで指定されたオブジェクトも自動的に生成・保存されます。
    class ProfileFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = Profile # Profileモデル (user [OneToOne], bio を持つ)
    
        user = factory.SubFactory(UserFactory) # Userを自動生成
        bio = factory.Faker('paragraph', locale='ja_JP')
    
    # Profileを作成すると、関連するUserも作成される
    profile = ProfileFactory.create()
    print(profile.user.username)
  • factory.RelatedFactory: 一対多や多対多(ManyToMany)の逆方向のリレーションシップで使用します。関連付けられる側のファクトリ(例: ArticleFactory)内で、関連付ける側のオブジェクト(例: Comment)を複数生成する場合などに使います。@factory.post_generationデコレータと組み合わせて使うことが多いです(後述)。
    class CommentFactory(factory.django.DjangoModelFactory):
        class Meta:
            model = Comment # Commentモデル (article [ForeignKey], text を持つ)
    
        article = factory.SubFactory(ArticleFactory)
        text = factory.Faker('sentence', locale='ja_JP')
    
    class ArticleFactory(factory.django.DjangoModelFactory):
        # ... (他の属性定義) ...
    
        # この記事に関連するコメントを3つ自動生成する
        comments = factory.RelatedFactory(CommentFactory, factory_related_name='article', size=3)
    
    # Articleを作成すると、関連するCommentも3つ作成される
    article = ArticleFactory.create()
    print(article.comment_set.count()) # => 3
    factory_related_nameには、関連先のモデル(この場合はComment)から関連元のモデル(Article)を参照するためのフィールド名を指定します。sizeで生成する数を指定できます。

特性 (`factory.Trait`)

「特性(Trait)」は、特定の状態や属性の組み合わせを持つオブジェクトを簡単に生成するための仕組みです。例えば、「非公開の記事」「管理者ユーザー」のようなバリエーションを定義できます。

class ArticleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Faker('sentence')
    published_at = factory.LazyFunction(timezone.now)
    author = factory.SubFactory(UserFactory)

    # 'unpublished' という特性を定義
    class Params:
        unpublished = factory.Trait(
            published_at=None
        )
        long_content = factory.Trait(
            content=factory.Faker('text', max_nb_chars=5000)
        )

# 通常の記事を作成 (公開状態)
article1 = ArticleFactory.create()
print(article1.published_at) # => (現在時刻)

# 'unpublished' 特性を適用して記事を作成 (非公開状態)
article2 = ArticleFactory.create(unpublished=True)
print(article2.published_at) # => None

# 複数の特性を適用
article3 = ArticleFactory.create(unpublished=True, long_content=True)
print(article3.published_at) # => None
print(len(article3.content)) # => (長いコンテンツ)

# build戦略でも特性を使える
article4 = ArticleFactory.build(unpublished=True)

class Params:内にfactory.Traitを使って特性を定義します。特性を適用するには、ファクトリ呼び出し時に特性名=Trueのように指定します。

ファクトリの継承

共通の設定を持つファクトリを複数定義したい場合、クラスの継承を利用できます。ベースとなるファクトリを定義し、それを継承して特定の属性を変更したり追加したりします。

# ベースとなるユーザーファクトリ
class BaseUserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
        abstract = True # このファクトリ自体は直接使わない

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    email = factory.LazyAttribute(lambda o: f'{o.first_name.lower()}.{o.last_name.lower()}@example.com')
    is_active = True

# 通常ユーザー用のファクトリ
class RegularUserFactory(BaseUserFactory):
    is_staff = False
    is_superuser = False

# 管理者ユーザー用のファクトリ
class AdminUserFactory(BaseUserFactory):
    is_staff = True
    is_superuser = True
    username = factory.Sequence(lambda n: f'admin{n}')

Metaクラスにabstract = Trueを指定すると、そのファクトリは抽象ベースクラスとなり、直接インスタンスを生成するためには使用できなくなります。共通の設定をまとめるのに便利です。

Post-generationフック (`@factory.post_generation`)

オブジェクトが生成・保存された後に、追加の処理を行いたい場合があります。特に多対多(ManyToMany)フィールドの設定や、関連オブジェクトの作成に便利です。@factory.post_generationデコレータを使用します。

# models.py
class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Post(models.Model):
    title = models.CharField(max_length=100)
    tags = models.ManyToManyField(Tag)

# factories.py
class TagFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Tag
        django_get_or_create = ('name',)
    name = factory.Sequence(lambda n: f'Tag {n}')

class PostFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Post

    title = factory.Faker('sentence')

    # Postが作成された後に呼ばれるフック
    @factory.post_generation
    def tags(self, create, extracted, **kwargs):
        # 'create' は呼び出し元の戦略がcreateかどうかのbool値
        # 'extracted' はファクトリ呼び出し時に 'tags' 引数で渡された値
        if not create:
            # build戦略などの場合は何もしない
            return

        if extracted:
            # PostFactory(tags=['tag1', 'tag2']) のように渡された場合
            if isinstance(extracted, (list, tuple)):
                for tag_name_or_obj in extracted:
                    if isinstance(tag_name_or_obj, str):
                        tag, _ = Tag.objects.get_or_create(name=tag_name_or_obj)
                        self.tags.add(tag)
                    elif isinstance(tag_name_or_obj, Tag):
                        self.tags.add(tag_name_or_obj)
            # PostFactory(tags=5) のように数が渡された場合は、その数だけTagを生成して追加
            elif isinstance(extracted, int):
                 tags_to_add = TagFactory.create_batch(extracted)
                 self.tags.add(*tags_to_add)
        else:
            # デフォルトで3つのタグを追加する
            tags_to_add = TagFactory.create_batch(3)
            self.tags.add(*tags_to_add)

# デフォルトで3つのタグを持つPostを作成
post1 = PostFactory.create()
print(post1.tags.count()) # => 3

# 指定した名前のタグを持つPostを作成
post2 = PostFactory.create(tags=['python', 'django', 'testing'])
print([tag.name for tag in post2.tags.all()]) # => ['python', 'django', 'testing']

# 5つのランダムなタグを持つPostを作成
post3 = PostFactory.create(tags=5)
print(post3.tags.count()) # => 5

post-generationフックメソッド(この例ではtags)は、オブジェクト(self)、createフラグ、extracted値を受け取ります。extractedには、ファクトリ呼び出し時にフックメソッド名と同じ名前の引数で渡された値が入ります。これにより、柔軟なデータ設定が可能になります。

factory.RelatedFactoryは、このようなpost-generationフックの一般的なパターンを簡略化するためのものです。

ファクトリ間の連携 (`factory.SelfAttribute`, `factory.ContainerAttribute`)

  • factory.SelfAttribute: 同じファクトリ内の他の属性の値を参照するために使用します。LazyAttributelambda o: o.other_attributeと似ていますが、より簡潔に書けます。ネストした属性(例: user.email)も参照できます。
    class ProfileFactory(factory.django.DjangoModelFactory):
                class Meta:
                    model = Profile
    
                user = factory.SubFactory(UserFactory)
                # user属性のemailを参照
                notification_email = factory.SelfAttribute('user.email')
                slug = factory.SelfAttribute('user.username')
  • factory.ContainerAttribute: (やや高度) ファクトリのコンテナ(通常はファクトリクラス自体)の属性やメソッドにアクセスする必要がある場合に使用します。例えば、ファクトリクラスに定義されたヘルパーメソッドを使いたい場合など。
    class ReportFactory(factory.django.DjangoModelFactory):
                class Meta:
                    model = Report
    
                # ファクトリクラスに定義されたヘルパーメソッド
                @classmethod
                def _generate_report_code(cls, prefix='REP'):
                     return f"{prefix}-{random.randint(1000, 9999)}"
    
                # ContainerAttributeを使ってクラスメソッドを呼び出す
                report_code = factory.ContainerAttribute(
                    lambda container: container._generate_report_code(prefix='DAILY')
                )
                title = factory.LazyAttribute(lambda o: f"Report {o.report_code}")

実践的なテクニック 💡

Djangoでの利用例 (テストコード内)

Djangoのテストコード(通常tests.pytests/ディレクトリ内)でfactory_boyを使う例を見てみましょう。unittestpytestと組み合わせて使います。

# tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from .factories import UserFactory, ArticleFactory
from django.utils import timezone

class ArticleViewTests(TestCase):

    def setUp(self):
        # テスト用のクライアントとユーザーを作成
        self.client = Client()
        self.user = UserFactory.create()
        self.client.force_login(self.user) # テストクライアントでログイン

    def test_article_list_view(self):
        """記事一覧ページが表示され、公開済みの記事が含まれることを確認"""
        # テストデータ作成
        ArticleFactory.create_batch(3, author=self.user, published_at=timezone.now())
        ArticleFactory.create(author=self.user, published_at=None) # 非公開記事

        # URLを取得してGETリクエスト
        url = reverse('article-list') # URL名を 'article-list' と仮定
        response = self.client.get(url)

        # アサーション
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'articles/article_list.html') # テンプレート名を確認
        self.assertEqual(len(response.context['articles']), 3) # コンテキスト内の記事数が3件であることを確認

    def test_article_detail_view(self):
        """記事詳細ページが表示されることを確認"""
        article = ArticleFactory.create(author=self.user)
        url = reverse('article-detail', kwargs={'pk': article.pk}) # URL名を 'article-detail' と仮定
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, article.title) # レスポンスに記事タイトルが含まれるか

    def test_article_create_view_post(self):
        """記事作成フォームが正しく処理されることを確認"""
        url = reverse('article-create') # URL名を 'article-create' と仮定
        data = ArticleFactory.attributes(author=self.user.pk) # 属性辞書を取得し、authorをIDに差し替え
        # content属性だけ上書き
        data['content'] = '新しい記事の内容です。'
        # published_atはフォームに含まれないと仮定

        response = self.client.post(url, data)

        # リダイレクトされるか、または成功を示すステータスコードを確認 (実装による)
        self.assertIn(response.status_code, [201, 302]) # Created or Redirect

        # 実際に記事が作成されたか確認
        from .models import Article
        self.assertTrue(Article.objects.filter(content='新しい記事の内容です。').exists())

テストケースごとに必要なデータだけをファクトリで生成することで、テストの独立性を保ち、理解しやすくなります。setUpメソッドで共通のユーザーなどを作成し、各テストメソッド内で個別のデータを作成するのが一般的です。

SQLAlchemyでの利用例

SQLAlchemyを使用している場合も同様に、SQLAlchemyModelFactoryを使ってテストデータを生成できます。重要なのは、ファクトリに利用するSQLAlchemyセッションを渡すことです。

# models.py (SQLAlchemy)
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
engine = create_engine('sqlite:///:memory:') # インメモリDBを使用
Session = scoped_session(sessionmaker(bind=engine))

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    books = relationship("Book", back_populates="author")

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    title = Column(String)
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship("Author", back_populates="books")

Base.metadata.create_all(engine) # テーブル作成

# factories.py
import factory
from .models import Author, Book, Session

class AuthorFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = Author
        sqlalchemy_session = Session # 使用するセッションを指定
        sqlalchemy_session_persistence = 'flush' # create後にflushを実行 (commitも可)

    id = factory.Sequence(lambda n: n + 1)
    name = factory.Faker('name')

class BookFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = Book
        sqlalchemy_session = Session
        sqlalchemy_session_persistence = 'flush'

    id = factory.Sequence(lambda n: n + 1)
    title = factory.Faker('sentence', nb_words=4)
    author = factory.SubFactory(AuthorFactory)

# tests.py (例: pytestを使用)
import pytest
from .models import Session, Author, Book
from .factories import AuthorFactory, BookFactory

@pytest.fixture(scope='function', autouse=True)
def db_session():
    """テストごとにセッションをクリアするフィクスチャ"""
    Session.remove() # 前のテストの影響を排除
    yield Session
    Session.remove()

def test_create_book_with_author(db_session):
    # BookFactory.create() を呼ぶと Author も生成され、DBにflushされる
    book = BookFactory.create(title="SQLAlchemyテスト")

    # セッションからクエリして確認
    queried_book = db_session.query(Book).filter_by(title="SQLAlchemyテスト").one_or_none()
    assert queried_book is not None
    assert queried_book.author is not None
    assert queried_book.author.name is not None

    queried_author = db_session.query(Author).filter_by(id=book.author.id).one_or_none()
    assert queried_author is not None

def test_create_multiple_books(db_session):
    author = AuthorFactory.create()
    # 同じ著者で複数の本を作成
    BookFactory.create_batch(3, author=author)

    assert db_session.query(Book).filter_by(author_id=author.id).count() == 3

Meta.sqlalchemy_sessionでセッションを指定することが重要です。テストフレームワーク(例: pytest)のフィクスチャ機能を使って、テストごとにセッションをクリーンアップ(Session.remove()など)するのが一般的です。sqlalchemy_session_persistencecreate後のセッションの扱い('flush'または'commit')を指定できます。

テストデータの一貫性を保つ方法

テストは再現可能であるべきです。Fakerなどを使ってランダムなデータを生成する場合、テストが失敗したときに原因を特定しにくくなることがあります。一貫性を保つために以下の方法が考えられます。

  • ランダムシードの固定: Fakerやfactory_boyが内部で使用するランダム生成器のシード値をテスト実行前に固定します。
    import factory.random
    import random
    import faker
    
    SEED = 12345
    
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        random.seed(SEED)
        factory.random.reseed_random(SEED)
        # Fakerインスタンスを取得してシードを設定 (もし直接使っている場合)
        # fake = faker.Faker()
        # faker.Faker.seed(SEED)
    pytestなどでは、テスト実行全体でシードを固定するプラグインもあります。
  • 重要な値は明示的に指定: テストのアサーションに直接関わるような重要な値は、ランダム生成に頼らず、ファクトリ呼び出し時に明示的に指定します。
    # 価格が1000円であることをテストしたい場合
    product = ProductFactory.create(price=1000) # ランダム生成ではなく固定値を指定
    self.assertEqual(product.calculate_tax(), 80) # 期待値が明確
  • 特性(Trait)の活用: 特定の条件(例: 特定のステータス)を持つデータを繰り返し使う場合は、Traitとして定義しておくと、テストコードが読みやすくなり、意図も明確になります。

大規模プロジェクトでのファクトリ管理

プロジェクトが大規模になると、ファクトリの数も増え、管理が複雑になることがあります。

  • ディレクトリ構成: Djangoの場合、各アプリケーションごとにtests/factories.pytests/factories/ディレクトリを作成してファクトリを配置します。共通で使うファクトリは、専用のcommonアプリやcoreアプリなどに配置することも考えられます。
  • ベースファクトリと継承: 共通の設定やロジックを持つモデル(例: タイムスタンプを持つモデル)に対応する抽象ベースファクトリを作成し、それを継承することでコードの重複を減らします。
  • 命名規則: ファクトリクラス名はモデル名 + Factory(例: UserFactory, ArticleFactory)のように一貫性を持たせると分かりやすくなります。
  • ドキュメント: 複雑なロジックを持つファクトリやTraitには、docstringやコメントでその目的や使い方を明記します。

注意点とベストプラクティス ⚠️

ファクトリの肥大化を防ぐ

一つのファクトリにあらゆるパターンのデータを生成させようとすると、Traitやpost-generationフックが増えすぎて複雑になりがちです。基本的な状態を生成するシンプルなファクトリを維持し、特定の状態はTraitで表現するか、あるいは別のファクトリとして(継承を使って)定義することを検討しましょう。

アンチパターン例:

class ComplicatedUserFactory(factory.django.DjangoModelFactory):
    # ... 多くの属性 ...
    class Params:
        admin = factory.Trait(is_staff=True, is_superuser=True)
        inactive = factory.Trait(is_active=False)
        with_profile = factory.Trait(profile=factory.SubFactory(ProfileFactory))
        with_orders = factory.Trait(orders=factory.RelatedFactory(OrderFactory, size=5))
        # ... さらに多くのTrait ...

    @factory.post_generation
    def assign_groups(self, create, extracted, **kwargs):
        # ... グループ割り当ての複雑なロジック ...
    # ... さらに多くのpost_generationフック ...

改善案:

  • 基本的なユーザーを作成するUserFactory
  • 管理者ユーザーを作成するAdminUserFactory(UserFactory)
  • プロファイルを持つユーザーが必要なら、テスト側でuser = UserFactory(profile=ProfileFactory())のように明示的に指定するか、UserProfileFactoryのような別のファクトリを作る。
  • 特定の関連データが必要な場合も、テスト側で生成するか、専用のヘルパー関数を用意する。

デフォルト値の適切な設定

ファクトリのデフォルト値は、そのオブジェクトが「有効な(valid)」状態になるための最小限の値に留めるのが良いとされています。テストに関係ないフィールドまでデフォルト値を設定すると、モデルの変更時にファクトリの修正箇所が増えたり、テストの意図が不明確になったりする可能性があります。

フィールドがnullable(null=True)であれば、ファクトリのデフォルト値を設定しないか、Noneにしておくことを検討します。テスト側で必要な値だけを指定する方が、テストの意図が明確になります。

推奨されるプラクティス(DjangoCon US 2022での発表より):

  • ファクトリは対応するモデルを表現するべき。
  • ファクトリのデフォルト値に依存しすぎない。
  • ファクトリは必須データのみを持つべき。NullableなフィールドはTraitで表現する。

`create()` vs `build()` の使い分け

前述の通り、create()はDBアクセスを伴うため低速です。可能な限りbuild()を使用することを検討しましょう。

  • モデルのメソッド(DBアクセスなし)のテスト → build()
  • フォームのバリデーションテスト(DBアクセスなし) → build()またはattributes()
  • ビューのテスト(DBアクセスあり) → create()
  • 複数のオブジェクト間のリレーションシップが重要なテスト → create()

テストスイート全体の実行時間を短縮するために、build()を積極的に使うことを意識すると良いでしょう。

パフォーマンスへの影響

大量のテストデータを生成する場合、特にcreate()戦略や複雑な関連(SubFactory, RelatedFactory)を多用すると、テストの実行時間が長くなる可能性があります。

  • create_batch()/build_batch()は、個別にcreate()/build()をループで呼び出すよりも効率的な場合があります。
  • テストに必要な最小限のデータのみを生成するように心がけます。
  • 不要な関連オブジェクトまで生成しないように、SubFactoryRelatedFactoryの使い方を見直します。Traitを使って関連オブジェクトの生成をオプションにするなどの方法があります。
  • プロファイリングツールを使って、テストスイートのどこで時間がかかっているかを特定し、ボトルネックとなっているファクトリやテストを改善します。

まとめ 🏁

factory_boyは、Pythonにおけるテストデータ生成のための非常に強力で柔軟なライブラリです。宣言的な構文、ORMとの連携、動的な値生成、関連オブジェクトの扱い、Traitによるバリエーション表現など、豊富な機能を提供し、テストコードの効率、可読性、保守性を大幅に向上させます。

基本的な使い方から、シーケンス、遅延属性、関連、特性、継承、post-generationフックといった高度な機能まで理解することで、複雑なテストシナリオにも対応できるようになります。

一方で、ファクトリの肥大化を防ぎ、適切なデフォルト値を設定し、create()build()を使い分けるといったベストプラクティスを意識することも重要です。

まだfactory_boyを使ったことがない方は、ぜひ導入を検討してみてください。テストデータ作成のストレスが軽減され、より品質の高いソフトウェア開発につながるはずです!🎉

さらに深く学びたい場合は、公式ドキュメントを参照することをお勧めします。様々なレシピや詳細なリファレンスが提供されています。

コメント

タイトルとURLをコピーしました