Django / Pytest / Splinter : IntegrityError duplicate key in test only
I know it's a very common problem and I read a lot of similar questions. But I can't find any solution, so, here I am with the 987th question on Stackoverflow about a Django Integrity error.
I'm starting a Django project with a Postgres db to learn about the framework. I did the classic Profile creation for users, automated with a post_save signal. Here is the model:
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
description = models.TextField(max_length=280, blank=True)
contacts = models.ManyToManyField(
"self",
symmetrical=True,
blank=True
)
And this is the signal that goes with it :
def create_profile(sender, instance, created, **kwargs):
if created:
user_profile = Profile(user=instance)
user_profile.save()
post_save.connect(create_profile, sender=User, dispatch_uid="profile_creation")
The project is just starting, and for now I only create users in the admin view. With the post_save signal, it's supposed to create a Profile with the same form.
Here is the admin setup :
class ProfileInline(admin.StackedInline):
model = Profile
class UserAdmin(admin.ModelAdmin):
model = User
list_display = ["username", "is_superuser"]
fields = ["username", "is_superuser"]
inlines = [ProfileInline]
I'm using pytest and Splinter for my test, and this is the integration test that don't work :
@pytest.mark.django_db
class TestAdminPage:
def test_profile_creation_from_admin(self, browser, admin_user):
browser.visit('/admin/login/')
username_field = browser.find_by_css('form input[name="username"]')
password_field = browser.find_by_css('form input[name="password"]')
username_field.fill(admin_user.username)
password_field.fill('password')
submit = browser.find_by_css('form input[type="submit"]')
submit.click()
browser.links.find_by_text("Users").click()
browser.links.find_by_partial_href("/user/add/").click()
browser.find_by_css('form input[name="username"]').fill('Super_pseudo')
browser.find_by_css('textarea[name="profile-0-description"]').fill('Super description')
browser.find_by_css('input[name="_save"]').click()
assert browser.url is '/admin/auth/user/'
assert Profile.objects.last().description is 'Super description'
when I run this, I get this error :
django.db.utils.IntegrityError: duplicate key value violates unique constraint "profiles_profile_user_id_key"
DETAIL: Key (user_id)=(2) already exists.
At first, I also saw this error when I was creating a user using my local server. But only if I wrote a description. If I let the description field empty, everything was working fine. So I wrote this integration test, to solve the issue. And then I read a lot, tweaked a few things, and the error stopped happening in my local browser. But not in my test suite.
So I used a breakpoint, there :
def create_profile(sender, instance, created, **kwargs):
breakpoint()
if created:
user_profile = Profile(user=instance)
user_profile.save()
And that's where the fun begins. This single test is calling the signal 3 times.
The first time it's called by the admin_user
fixture that I'm using.
(Pdb) from profiles.models import Profile
(Pdb) instance
<User: admin>
(Pdb) created
True
(Pdb) instance.profile
*** django.contrib.auth.models.User.profile.RelatedObjectDoesNotExist: User has no profile.
(Pdb) Profile.objects.count()
0
(Pdb) continue
Seems legit, the admin user don't have a profile, why not. Then the signal is called again on the same instance.
(Pdb) instance
<User: admin>
(Pdb) instance.profile
<Profile: admin>
(Pdb) Profile.objects.count()
1
(Pdb) Profile.objects.last()
<Profile: admin>
(Pdb) created
False
(Pdb) continue
Still legit, weird, but it's not doing anything. created
is False, so it's not creating a second profile. Didn't need the first one, but it's not making the test fail. And then :
(Pdb) instance
<User: Super_pseudo>
(Pdb) created
True
(Pdb) instance.profile
<Profile: Super_pseudo>
(Pdb) Profile.objects.count()
1
(Pdb) Profile.objects.last()
<Profile: admin>
This is so weird. The profile is not saved, but it's raising an Integrity error anyway. It looks instanciated, when I call instance.profile
I get something, but it don't look like it's saved in the db. But the error happens anyway. I have no clue, I spent a few hours already, and I don't know what to look.
Feels like I'm missing something important, and that's why I'm asking for your help.
Edit
I tried updating the signal with if created and not kwargs.get('raw', False):
, but it doesn't work.