You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的db fixture自动回滚事务,避免测试数据残留

5. 替换内存对象判断为数据库查询

如果之前用hasattr(instance, 'profile')判断,可能因为内存对象状态与数据库不一致导致误判,直接改用数据库查询更可靠,参考方案1中的代码。

内容的提问来源于stack exchange,提问作者Ruff9

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 10:10:29