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
とする
- 今回は、Author.publicationsなので、
- 引数は、
(self, create, extracted, **kwargs)
と、定型create
がFalseの場合、build()などで生成extracted
に、build()で指定した引数の値が入ってくる
- メソッド名は、Modelのmodels.ManyToManyField名
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を考えます。
- Many-to-many relation with a ‘through’ - Common recipes — Factory Boy 2.7.0 documentation
- python - factory_boy: add several dependent objects - Stack Overflow
今回作成するFactoryは、4つです。
- モデルPersonを生成するFactory
- モデルGroupを生成するFactory
- モデルMembershipを生成するFactory
- 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の変更
<Factory>.create(name='変更後の値')
を渡す
- Group.name
- GroupFactoryでgroupを事前に生成し、create()時に
membership1__group
に渡す - create()時に
membership2__group__name='g-2'
のように、手繰って渡す - Factory内のRelatedFactoryに、
group__name='Group1'
のように定義して渡す
- GroupFactoryでgroupを事前に生成し、create()時に
などの方法があります。
# 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