こんにちは。株式会社リーディングマークエンジニアの矢代と申します。
RailsのFactoryBotっていいですよね!私は1年半ほどしかRailsの実務経験はありませんが、DjangoのFactory使ってみたり、Djangoの開発をしていると、ああ、Railsの引いてくれていたレールに脳死で乗ってしまっていた部分もあったんだなあと日々、開発者として未熟な自分を痛感する日々であります。
Leadingmarkでの勤務が1年ほどに達してDjangoでもしっかりとテストを書くようになってきまして、今回Fakerの使い方で面白い点があったので記事を書きました。
動作環境
- MacBook Pro 13-inch, M1, 2020
- Python 3.12
- Django 4.2
- factory-boy 3.3.0
- Faker 25.2.0
今回例で使う3つのモデルとその関係
まずは書いてみた
DjangoにもRailsみたいなFactoryある!となって最初に書いたFactoryはこちら
from factory.django import DjangoModelFactory
from faker import Faker
fake = Faker('ja-JP')
class ClientFactory(DjangoModelFactory):
class Meta:
model = 'myapp.Client'
code = str(fake.random_int(min=0, max=9999)).zfill(4)
name = fake.romanized_name()
address = fake.address()
この方法のFakerの落とし穴
実際に呼び出してみると わかる。特に着目していただきたいのが、ユニークにしたい部分があるのですが、2回作成しようとするとエラーになります
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory()
>>> client_2 = ClientFactory()
Traceback (most recent call last):
~~~
django.db.utils.IntegrityError: (1062, "Duplicate entry '7878' for key 'myapp_client.PRIMARY'")
なぜ主キーの重複が起きるのか
これはPython特有?なのかわかりませんが、クラス直下にカラム名を記述して値を代入すると、みた感じ作成はできる(client_1 = ClientFactory()は通っているため)のですが、読み込み時に値が決定する方式らしく、読み込み時に「ABC」になったら「ABC」のまま2回目も作成しようとするのです。
一意にしたい顧客コードなのにFactoryの時点でこれはダメですね…
解決策
技術ブログなどを漁ってみたところ、LazyAttribute使えよBro!と書いてあったと思うので使ってみます。
class ClientFactory(DjangoModelFactory):
class Meta:
model = 'myapp.Client'
code = factory.LazyAttribute(lambda o: f'{fake.random_int(min=0, max=9999):04}')
name = factory.LazyAttribute(lambda o: f'{fake.name()}')
address = factory.LazyAttribute(lambda o: f'{fake.address()}')
ちゃんと作成できた
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory()
>>> client_2 = ClientFactory()
>>> client_1.number
'6994'
>>> client_2.number
'1672'
後学のために公式ドキュメントを探し当てたので置いておきます https://factoryboy.readthedocs.io/en/stable/reference.html#lazyattribute
そういえば、0埋めのzfillってこういう書き方(:04)もできたなあと思い出した
この方法の所感
- 正直、何個も関数に囲まれるのはちょっと…
- 超細かいですがLazyAttribute始まる地点が違くてちょっと読みづらい…?
- え、lambda使わないといけないんすか…
代替案として、Pythonのdict(辞書)にあ るsetdefaultが便利でした
ここでRailsとは違った自由度の高いソリューションが見つかった(ソリューションって言ってみたかっただけ) 表題にあるとおりの関数でFactoryを作ってみると、なんと、Fakerの呼び出しにLazyAttributeがなくても、都度呼び出して値をセットしてくれるのです
from factory.django import DjangoModelFactory
import factory
from faker import Faker
fake = Faker('ja-JP')
class ClientFactory(DjangoModelFactory):
class Meta:
model = 'myapp.Client'
@classmethod
def _create(cls, model_class, *args, **kwargs):
kwargs.setdefault('code', f'{fake.random_int(min=0, max=9999):04}')
kwargs.setdefault('name', f'{fake.name()}')
kwargs.setdefault('address', f'{fake.address()}')
return super()._create(model_class, *args, **kwargs)
実行結果(ほぼ同じにはなりますが)
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory.create()
>>> client_2 = ClientFactory.create()
>>> client_1.number
'9091'
>>> client_2.number
'3501'
setdefaultは、辞書の中にそのキーがなければ、第2引数のものを使うというメソッドで、都度呼び出しをしてくれます。 この方法なら、クラス直下宣言していたカラムたちを持ってきても特に違和感がない。というかぶっちゃけこの書き方は割と好きなだけかもしれません。 これでこのFactoryクラスは、RailsのFactoryBotに大きく近づいたのです
最後に
まだまだDjangoは修行中。書くのはノイズかな〜と思ったりした事項もあったので、その点は勉強会などでLTにしようと思います。 しばらくはDjangoのテスト関連の記事を書くと思うので、よろしければ、いいねやシェアで応援していただけると嬉しいです。 最後まで読んでいただきありがとうございました。