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.
I tried updating the signal with
if created and not kwargs.get('raw', False):, but it doesn't work.