読者です 読者をやめる 読者になる 読者になる

Django + factory_boyで、1対多や多対多のリレーションを持つテストデータを作る

Django Python テスト pytest

DjangoでModelまわりのテストデータを用意するには何を使うのが良いのかを調べてみたところ、factory_boyを使うのが良さそうでした。Djangoの他、Mogo・MongoEngine・SQLAlchemyなどのORMにも対応しているのも良いです。
rbarrois/factory_boy: A test fixtures replacement for Python

 
そこで、DjangoのForeignKeyやManyToManyFieldなどのリレーションまわりのテストデータを作ってみた時のメモを残します。

今回の目次です。  

 

環境

 
以下の流れでDjangoアプリを作成しておきます。

d:\Sandbox\Django_factory_boy_sample>virtualenv -p c:\python35-32\python.exe env

d:\Sandbox\Django_factory_boy_sample>env\Scripts\activate
(env) d:\Sandbox\Django_factory_boy_sample>pip install django pytest-django factory-boy

(env) d:\Sandbox\Django_factory_boy_sample>django-admin startproject myproject .

(env) d:\dev\sandbox\pytest-django-factoryboy>mkdir apps\myapp
(env) d:\Sandbox\Django_factory_boy_sample>python manage.py startapp myapp

 

ForeignKeyの場合

以下のModelで考えます。

from django.db import models
class Parent(models.Model):
    name = models.CharField(max_length=100)
    
class Child(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey(Parent)

 
ER図は以下の通りです。

+--------+  1:n    +-------+
| Parent | <=====> | Child |
+--------+         +-------+

 

親Model生成時に子Modelも生成

RelatedFactoryを使います。
RelatedFactory - Reference — Factory Boy 2.7.0 documentation

class ParentToChild_ChildFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Child
    name = 'child1'

class ParentToChild_ParentFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Parent
    name = 'parent1'
    relatedparent = factory.RelatedFactory(ParentToChild_ChildFactory, 'parent')

親Modelを生成するParentToChild_ParentFactoryでは、RelatedFactory用のフィールドを用意しますが、

  • フィールド名にアンダースコア(_)を含めるとエラー
  • RelatedFactory()の第2引数は、子ModelのForeignKeyフィールド名

となることに注意します。
python - factory_boy: add several dependent objects - Stack Overflow

 
ParentToChild_ParentFactoryを使って生成と確認をします。

p1 = ParentToChild_ParentFactory()
print(p1.name)                    #=> parent1
print(p1.child_set.all()[0].name) #=> child1

 
なお、親に紐づく子Modelは、<モデル名の小文字>_setという名前でアクセスします。
Following relationships “backward" - Making queries | Django documentation | Django

また、子Modelのフィールド(child_set)はRelatedManagerクラスのため、all()でアクセスします。
Related objects - Making queries | Django documentation | Django

print(p1.child_set.__class__.__name__) #=> RelatedManager
print(p1.child_set)                    #=> myapp.Child.None
print(p1.child_set.all())              #=> [<Child: Child object>]

 

Factoryで定義したフィールド値を変更

Factoryの定義とは異なるフィールド値へ変更したい場合、生成時にFactoryクラスの引数へと設定します。

子Modelのフィールド値を変更したい場合には、<RelatedFactoryのフィールド名>__<対象の子Modelのフィールド名>とします。
RelatedFactory - Reference — Factory Boy 2.7.0 documentation

p2 = ParentToChild_ParentFactory(
    name='parent_factory', child__name='child_factory')
print(p2.name)                    #=> parent_factory
print(p2.child_set.all()[0].name) #=> child_factory

 

子Model生成時に親Modelも生成

SubFactoryを使います。
SubFactory - Reference — Factory Boy 2.7.0 documentation

class ChildToParent_ParentFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Parent
    name = 'parent2'
    
class ChildToParent_ChildFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Child
    name = 'child2'
    parent = factory.SubFactory(ChildToParent_ParentFactory, name='parent_value')

 
子Modelを生成するChildToParent_ChildFactoryでは、SubFactory用のフィールドを用意しますが、

  • SubFactoryのフィールド名と子ModelのForeignKeyフィールド名が一致(今回はparent)

という点に注意します。

 
ChildToParent_ChildFactoryを使って生成と確認をします。

c1 = ChildToParent_ChildFactory()
print(c1.parent.name) #=> parent_value
print(c1.name)        #=> child2

 

親Modelのフィールドに、子のフィールド値をコピー
parent = factory.SubFactory(
    ChildToParent_ParentFactory, 
    name=factory.SelfAttribute('..name'))

のようにfactory.SelfAttribute('..<子Modelのフィールド名>')を使います。
Copying fields to a SubFactory - Common recipes — Factory Boy 2.7.0 documentation

 
生成と結果は以下の通りです。

c2 = ChildToParent_ChildWithCopyFactory()
print(c2.parent.name) #=> child2
print(c2.name)        #=> child2

 

同じ親Modelを持つ子Modelの生成

SubFactoryやRelatedFactoryを使って同じ親Modelを持つ子Modelを1回で生成しようとしましたが、うまい方法が思いつきませんでした。

そのため、

class SameParent_ParentFactroy(factory.django.DjangoModelFactory):
    class Meta:
        model = Parent
    name = 'parent_same'

class SameParent_ChildFactroy(factory.django.DjangoModelFactory):
    class Meta:
        model = Child
    name = 'child_same'

というFactoryを用意し、

p = SameParent_ParentFactroy()
c1 = SameParent_ChildFactroy(name='child1', parent=p)
c2 = SameParent_ChildFactroy(name='child2', parent=p)

print('child_pk:{pk} - child_name:{name}, parent_pk:{p_pk}'
        .format(pk=c1.pk, name=c1.name, p_pk=c1.parent.pk))
#=> child_pk:1 - child_name:child1, parent_pk:1

print('child_pk:{pk} - child_name:{name}, parent_pk:{p_pk}'
        .format(pk=c2.pk, name=c2.name, p_pk=c2.parent.pk))
#=> child_pk:2 - child_name:child2, parent_pk:1

のように生成しました。

他に良い方法があれば、教えていただけるとありがたいです。

 

ManyToManyField・through無しの場合

以下のModelで考えます。
出典元:Many-to-many relationships | Django documentation | Django

class Publication(models.Model):
    title = models.CharField(max_length=30)
    
class Author(models.Model):
    headline = models.CharField(max_length=100)
    publications = models.ManyToManyField(Publication)

 
ER図は以下の通りです。中間テーブルはDjango任せです。

+-------------+  n:n    +--------+
| Publication | <=====> | Author |
+-------------+         +--------+

 

基本的な生成

Authorモデルを生成した時にPublicationモデルも生成するには、AuthorのFactoryに@factory.post_generationデコレータを定義・使用します。
Simple Many-to-many relationship - Common recipes — Factory Boy 2.7.0 documentation

class M2MSimple_PublicationFactory(factory.django.DjangoModelFactory):
    title = factory.Sequence(lambda n: "Title #%s" % n)
    
    class Meta:
        model = Publication
        
class M2MSimple_AuthorFactory(factory.django.DjangoModelFactory):
    headline = 'm2m_simple_headline'
    
    class Meta:
        model = Author
        
    @factory.post_generation
    def publications(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            for publication in extracted:
                self.publications.add(publication)

上記のコードで考えたことは、以下の通りです。

  • @factory.post_generationデコレータについて
    • メソッド名は、Modelのmodels.ManyToManyField名
      • 今回は、Author.publicationsなので、publicationsとする
    • 引数は、(self, create, extracted, **kwargs)と、定型
      • createがFalseの場合、build()などで生成
      • extractedに、build()で指定した引数の値が入ってくる
  • factory.Sequence()で連番を生成

 
あとは、複数のPublicationモデルを生成した後、AuthorFactory.create()した時にPublicationモデルを指定することで、同時に中間テーブルも作成されます。

pub1 = M2MSimple_PublicationFactory.create()
pub2 = M2MSimple_PublicationFactory.create()
pub3 = M2MSimple_PublicationFactory.create()

a = M2MSimple_AuthorFactory.create(publications=(pub1, pub2, pub3))
print('pk:{pk}, headline:{headline}'.format(pk=a.pk, headline=a.headline))
#=> pk:1, headline:m2m_simple_headline

 
なお、publicationsへのアクセスは、

print(a.publications.__class__.__name__)      #=> ManyRelatedManager
print('all()なし: {}'.format(a.publications)) #=> myapp.Publication.None
print('all()あり: {}'.format(a.publications.all()))
#=> [<Publication: Publication object>, <Publication: Publication object>, <Publication: Publication object>]
print(a.publications.all()[0].title)          #=> Title #0

のため、all()が必要です。

 

ManyToManyField・throughありの場合

以下のModelで考えます。中間モデルとしてMembershipを明示的に定義しています。
出典元:Extra fields on many-to-many relationships - Models | Django documentation | Django

class Person(models.Model):
    name = models.CharField(max_length=128)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    remarks = models.CharField(max_length=50, default='')

 
ER図は以下の通りです。中間テーブルも自分で作ります。

+--------+  1:n    +------------+  n:1    +-------+
| Person | <=====> | Membership | <=====> | Group |
+--------+         +------------+         +-------+

 

基本的な生成

以下を参考に、必要なFactoryを考えます。

 
今回作成するFactoryは、4つです。

  1. モデルPersonを生成するFactory
  2. モデルGroupを生成するFactory
  3. モデルMembershipを生成するFactory
  4. 1.のFactoryを継承し、MembershipのFactoryをRelatedFactoryとして持つFactory (今回は、1:1と1:2の2種類を用意)
class M2M_Through_PersonFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Person
    name = 'person_name'

class M2M_Through_GroupFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Group
    name = 'group_name'

class M2M_Through_MembershipFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Membership
    person = factory.SubFactory(M2M_Through_PersonFactory)
    group = factory.SubFactory(M2M_Through_GroupFactory)

# 1Personで1Groupを持つMembershipのFactory
class M2M_Through_PersonWithGroupFactory(M2M_Through_PersonFactory):
    membership = factory.RelatedFactory(M2M_Through_MembershipFactory, 'person')

# 1Personで2Groupを持つMembershipのFactory
class M2M_Through_PersonWithTwoGroupFactory(M2M_Through_PersonFactory):
    membership1 = factory.RelatedFactory(
        M2M_Through_MembershipFactory, 'person')
    membership2 = factory.RelatedFactory(
        M2M_Through_MembershipFactory, 'person')

 
1:1で生成するFactoryの結果です。

p = M2M_Through_PersonWithGroupFactory.create()
print(p.membership_set.all()[0].person.name) #=> person_name
print(p.membership_set.all()[0].group.name)  #=> group_name

 
1:2で生成するFactoryの結果です。

p = M2M_Through_PersonWithTwoGroupFactory.create()
print(p.membership_set.all()[0].person.name) #=> person_name
print(p.membership_set.all()[0].group.name)  #=> group_name
print(p.membership_set.all()[1].person.name) #=> person_name
print(p.membership_set.all()[1].group.name)  #=> group_name

 

Factoryで定義したフィールド値を変更

Person.nameやGroup.nameの値を変更する場合は、

  • Person.nameの変更
  • Group.name
    • GroupFactoryでgroupを事前に生成し、create()時にmembership1__groupに渡す
    • create()時にmembership2__group__name='g-2'のように、手繰って渡す
    • Factory内のRelatedFactoryに、group__name='Group1'のように定義して渡す

などの方法があります。

# Factory
class M2M_Through_PersonWithTwoGroup_Update_Factory(M2M_Through_PersonFactory):
    membership1 = factory.RelatedFactory(
        M2M_Through_MembershipFactory, 'person', group__name='Group1')
    membership2 = factory.RelatedFactory(
        M2M_Through_MembershipFactory, 'person', group__name='Group2')
    membership3 = factory.RelatedFactory(
        M2M_Through_MembershipFactory, 'person', group__name='Group3')

# 使い方
g = M2M_Through_GroupFactory.create(name='g-1')
p = M2M_Through_PersonWithTwoGroup_Update_Factory.create(
    name='re_person_name',
    membership1__group=g,
    membership2__group__name='g-2',
)
print(p.membership_set.all()[0].person.name) #=> re_person_name
print(p.membership_set.all()[0].group.name)  #=> g-1
print(p.membership_set.all()[1].person.name) #=> re_person_name
print(p.membership_set.all()[1].group.name)  #=> g-2
print(p.membership_set.all()[2].person.name) #=> re_person_name
print(p.membership_set.all()[2].group.name)  #=> Group3

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_factory_boy_sample