delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Django+factory-boyでテストデータを作成する際に「Duplicate entry」を回避する方法

概要

Django+factory-boyでテストデータを作成する際に、結合先のテーブルに主キーが同一のレコードを複数INSERTしようとして「Duplicate entry」が発生してしまう事象を回避する方法について書く。(詳細は後述)

前提

Django: 3.0.7
factory-boy: 2.12.0

結論

以下のように主キーの重複を回避したいモデルのFactoryクラスのMetaクラスにdjango_get_or_create = ("${主キー名}",)を定義する。

class PrefectureFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Prefecture
        django_get_or_create = ("prefecture_id",)
    
    prefecture_id = 13
    prefecture_name = "東京都"

詳細

以下のようなアンケートの回答結果を格納するテーブル(survey_response)を例とする。

アンケートの回答項目は「氏名(name)」と「住んでる県(prefecture_id)」で、prefecture_idで「都道府県マスター(prefecture)」に外部キー参照する。

[models.py]

from django.db import models

class Prefecture(models.Model):
    class Meta:
        db_table = "prefecture"
    prefecture_id = models.IntegerField(primary_key=True)
    prefecture_name = models.CharField(max_length=8, null=False)

class SurveyResponse(models.Model):
    class Meta:
        db_table = "survey_response"
    response_id = models.IntegerField(primary_key=True)
    prefecture = models.ForeignKey(
        to="prefecture",
        db_column="prefecture_id",
        to_field="prefecture_id",
        on_delete=models.PROTECT,
        null=True,
        related_name="survey_response",
    )

ユニットテストを実装するために、以下のようにfactories.pyとtests.pyを記述する。

[factories.py]

import factory

from survey.models import Prefecture, SurveyResponse

class PrefectureFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Prefecture
    
    prefecture_id = 13
    prefecture_name = "東京都"

class SurveyResponseFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = SurveyResponse
    
    response_id = 1
    prefecture = factory.SubFactory(PrefectureFactory)

[tests.py]

from django.test import TestCase
from survey.factories import SurveyResponseFactory

class TestModel(TestCase):
    @classmethod
    def setUpTestData(cls):
        SurveyResponseFactory.create(response_id=1)
        SurveyResponseFactory.create(response_id=2)
    
    def test_1(self):
        self.assertEqual(1, 1)

・この状態で

$ python manage.py test

を実行すると「Duplicate entry」が発生してしまう。

以下は、SQLiteの場合のエラーメッセージ

django.db.utils.IntegrityError: UNIQUE constraint failed: prefecture.prefecture_id

factory-boyは存在確認せずに「prefecture」テーブルにprefecture_id=13のレコードをINSERTしようとしてしまうことが原因である。

解決までの経緯

まずは以下のstackoverflowの記事を見つけた。
stackoverflow.com

この回答の通りに以下のように設定してみた。

class PrefectureFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Prefecture
    
    FACTORY_DJANGO_GET_OR_CREATE = ("prefecture_id",)
    prefecture_id = 13
    prefecture_name = "東京都"

testを実行すると、以下のエラーメッセージが発生した。

TypeError: Prefecture() got an unexpected keyword argument 'FACTORY_DJANGO_GET_OR_CREATE'

公式ドキュメントを確認したところ、「FACTORY_*」というクラス変数に設定値を定義するのは古いスタイルで、新しいスタイルでは設定値はMetaクラスに定義するとのことである。
factoryboy.readthedocs.io

Metaクラスに同等の設定値を定義する方法を調べたところ、以下のstackoverflowの記事を見つけた。
stackoverflow.com

上記の回答に最新の公式ドキュメントのリンクも貼ってくださっていた。
factoryboy.readthedocs.io

ドキュメントの通りに以下のように設定すると、「Duplicate entry」が発生しなくなった。

class PrefectureFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Prefecture
        django_get_or_create = ("prefecture_id",)
    
    prefecture_id = 13
    prefecture_name = "東京都"