Django测试出现IntegrityError:重复键值违反唯一约束
Django post_save信号测试时触发重复创建Profile的问题排查与解决
问题背景
基于Django + PostgreSQL搭建项目,通过post_save信号为User模型自动创建一对一关联的Profile模型,Admin后台配置了ProfileInline内联表单,本地浏览器操作创建用户并填写Profile描述完全正常。但使用pytest+Splinter执行集成测试时,抛出django.db.utils.IntegrityError,提示重复键值违反唯一约束profiles_profile_user_id_key,user_id=2已存在。断点调试发现测试中信号被调用3次,创建测试用户Super_pseudo时,instance内存中已存在Profile实例但未存入数据库,仍触发重复键错误,尝试在信号中添加if created and not kwargs.get('raw', False):判断无效。
可能的触发原因
- Admin内联与信号的逻辑冲突:Admin的ProfileInline在创建User时,会提前初始化Profile对象(未提交数据库),而post_save信号又会触发创建操作,两者叠加导致重复创建请求
- 测试环境信号重复注册:pytest运行时,模块重复导入可能导致同一个
post_save信号被多次注册,一次User创建触发多次信号执行 created判断的局限性:通过Admin内联创建User时,created为True,但Inline已经生成了Profile的内存对象,信号再次尝试创建就会和待提交的Profile产生唯一键冲突- 测试事务的状态不一致:Splinter测试可能涉及事务回滚或多次用户创建操作,导致内存对象状态与数据库实际状态不匹配,信号误判需要创建Profile
具体解决方案
1. 修改信号逻辑,优先检查数据库中是否存在Profile
把信号中的判断逻辑从仅依赖created,改为直接查询数据库确认Profile是否存在,避免重复创建:
from django.db.models.signals import post_save from django.contrib.auth.models import User from django.dispatch import receiver from .models import Profile @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): # 跳过数据导入等原始操作 if kwargs.get('raw', False): return # 直接查询数据库,确认是否已有关联的Profile if not Profile.objects.filter(user=instance).exists(): Profile.objects.create(user=instance)
2. 调整Admin内联配置,避免自动生成空表单
如果Admin内联已经允许用户在创建User时直接填写Profile信息,可以将extra设为0,避免Inline自动生成空的Profile表单,减少与信号的冲突:
from django.contrib import admin from django.contrib.auth.models import User from .models import Profile class ProfileInline(admin.StackedInline): model = Profile extra = 0 # 不自动生成空的Profile表单,仅在已有Profile时显示 class UserAdmin(admin.ModelAdmin): inlines = [ProfileInline] admin.site.unregister(User) admin.site.register(User, UserAdmin)
3. 确保测试环境中信号仅注册一次
在pytest的conftest.py中添加信号重置的fixture,避免模块重复导入导致信号多次注册:
import pytest from django.db.models.signals import post_save @pytest.fixture(autouse=True) def reset_signals(): # 保存原始的信号接收器列表 original_receivers = post_save.receivers.copy() yield # 测试结束后重置信号接收器,避免污染后续测试 post_save.receivers = original_receivers post_save.sender_receivers_cache.clear()
4. 规范测试中的用户创建逻辑
- 避免在测试中混合使用
User.objects.create()和浏览器操作创建用户,减少重复触发信号的可能 - 确保每个测试用例使用独立的数据库环境,依赖pytest-django的
dbfixture自动回滚事务,避免测试数据残留
5. 替换内存对象判断为数据库查询
如果之前用hasattr(instance, 'profile')判断,可能因为内存对象状态与数据库不一致导致误判,直接改用数据库查询更可靠,参考方案1中的代码。
内容的提问来源于stack exchange,提问作者Ruff9
相关产品推荐
相关产品推荐

