使用factory-boy创建Django模型时触发主键重复IntegrityError
Django测试中factory-boy生成UUID主键重复导致IntegrityError问题
运行测试时出现以下错误:
django.db.utils.IntegrityError: duplicate key value violates unique constraint "shops_shop_pkey" DETAIL: Key (id)=(cd84e095-7d1a-41fc-adb3-7f5db5e07d50) already exists. ========================================================================== short test summary info =========================================================================== FAILED tests/models/test_shop.py::test_shop_creation - django.db.utils.IntegrityError: duplicate key value violates unique constraint "shops_shop_pkey"
我用factory-boy创建了ShopModelFactory工厂类,用来生成Django的Shop模型,字段用faker填充,之后用pytest编写测试。数据库是空的,测试前也删除了所有记录,后台管理面板能正常创建记录,但测试始终报主键重复错误。
我的Shop模型
class Shop(TimedBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) first_name = models.CharField("First Name", max_length=20) last_name = models.CharField("Last Name", max_length=20) email = models.EmailField("Email", unique=True) phone = PhoneNumberField( "Phone Number", unique=True, ) shop_name = models.CharField("Shop Name", max_length=50, unique=True) shop_url = models.URLField("Shop URL", unique=True) logo = models.ImageField("Logo", upload_to="logos/", blank=True, null=True) banner = models.ImageField( "Banner", upload_to="banners/", blank=True, null=True, ) street1 = models.CharField("Street 1", max_length=50) street2 = models.CharField("Street 2", max_length=50, blank=True) city = models.CharField("Town / City", max_length=100) zip_code = models.CharField("Zip Code", max_length=10) country = CountryField("Country", blank_label="(select country)") state = models.CharField("State", max_length=100, blank=True, default="") password = models.CharField("Password") confirm_password = models.CharField("Confirm Password") is_visible = models.BooleanField( verbose_name="Is the shop visible in the shops catalog", default=True, ) class Meta: verbose_name = "Խանութ" verbose_name_plural = "Խանութներ" app_label = "shops" def __str__(self): return self.shop_name def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) if self.logo: instance = super().save(*args, **kwargs) logo_path = self.logo.path logo = Image.open(logo_path) logo_size = os.path.getsize(logo_path) # in bytes thirty_mb = 30 * 1024 * 1024 while logo_size > thirty_mb: logo = logo.convert("RGB") logo.save( logo_path, quality=60, optimize=True, format="JPEG", ) return instance
我的工厂类
import factory from factory.django import DjangoModelFactory, ImageField from api.v1.apps.shops.models.shops import Shop class ShopModelFactory(DjangoModelFactory): class Meta: model = Shop first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') email = factory.Faker('email') phone = factory.Faker('phone_number') shop_name = factory.Faker('company') shop_url = factory.Faker('url') logo = ImageField(color='blue') banner = ImageField(color='red') street1 = factory.Faker('street_address') street2 = factory.Faker('secondary_address') city = factory.Faker('city') zip_code = factory.Faker('postcode') country = factory.Faker('country_code') state = factory.Faker('state') password = factory.Faker('password') confirm_password = password is_visible = factory.Faker('boolean')
我的测试代码
@pytest.mark.django_db(transaction=True) def test_shop_creation(): Shop.objects.all().delete() # 生成实例前已删除所有数据库记录 shop = ShopModelFactory() assert Shop.objects.count() == 1 assert Shop.objects.first() == shop assert shop.first_name assert shop.last_name assert shop.email assert shop.phone assert shop.shop_name assert shop.shop_url assert shop.street1 assert shop.city assert shop.zip_code assert shop.country assert shop.state assert shop.password assert shop.confirm_password assert isinstance(shop.is_visible, bool)
问题原因及解决办法
save方法重复调用导致主键冲突
你的Shop模型save方法中,处理logo时额外调用了一次super().save(),这会导致同一个实例被保存两次:第一次保存时已生成UUID主键,第二次保存时尝试用相同主键再次插入,触发唯一约束错误。
修改后的save方法:def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) if self.logo: logo_path = self.logo.path logo = Image.open(logo_path) logo_size = os.path.getsize(logo_path) # in bytes thirty_mb = 30 * 1024 * 1024 while logo_size > thirty_mb: logo = logo.convert("RGB") logo.save( logo_path, quality=60, optimize=True, format="JPEG", ) # 重新获取压缩后的文件大小,避免无限循环 logo_size = os.path.getsize(logo_path) return instance显式指定工厂类的UUID生成逻辑
虽然模型设置了default=uuid.uuid4,但factory-boy可能不会正确触发默认值生成。在工厂类中显式添加id字段,确保每次生成新实例时生成唯一UUID:import uuid # 在ShopModelFactory中添加 id = factory.LazyFunction(uuid.uuid4)调整测试事务模式
使用@pytest.mark.django_db(transaction=True)会让测试在事务中运行,删除操作的效果可能受事务隔离影响。可以改用transaction=False,或使用pytest-django的默认django_db标记(默认不使用事务)。
内容的提问来源于stack exchange,提问作者user22795766
相关产品推荐
相关产品推荐

