Test fails with AssertionError: 404 != 200 when using Factory Boy

Context:

  • Test fails with AssertionError: 404 != 200 when I try to setup the test with Factory Boy
  • To make sure my error was something related to how I’m doing the factories/test and not the code it’s testing (I’m a newbie to learning Factory Boy), I created the same test but without using factories which does pass (see code excerpts).
  • Because of this example Django profile section in Common recipes — Factory Boy stable documentation, I thought (even though I’m not using a signal) that maybe the way I’m setting up the 1:1 field between CustomUser and UserProfile is the issue. Or something to do with how in the non-Factory Boy test, I set the created_by field as the user I created, but in the Factory Boy test, I don’t because it uses the UserProfileFactory to create it.

Question:

  • How would I adjust the test using Factory Boy to pass (and/or the factories it’s relying on)?

Any help appreciated, and code excerpts below:
nonprofits/tests/tests.py

class TestNonprofitViews(TestCase):

    def test_nonprofit_detailview_standard_user(self):
        nonprofit = NonprofitFactory.build(name="XYZ Nonprofit")

        authenticated_standard_user = CustomUserFactory.create(
            username="authenticatedstandarduser",
            password="secret",
            is_staff=False,
            is_superuser=False,
        )

        self.client.login(username="authenticatedstandarduser", password="secret")
        response = self.client.get(
            reverse(
                "nonprofit_detail",
                kwargs={"pk": nonprofit.id},
            )
        )

        self.assertEqual(response.status_code, 200)

# SAME TEST WITHOUT FACTORY BOY  -------------------------------------------------
class NonprofitTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(
            username="testuser1",
            email="test@email.com",
            password="testpass123",
        )

        cls.nonprofit = Nonprofit.objects.create(
            name="Nonprofit Name",
            created_by=cls.user.userprofile,
        )

    def test_nonprofit_detailview_authenticated_user(self):
        self.client.login(username="testuser1", password="testpass123")
        response = self.client.get(
            reverse("nonprofit_detail", kwargs={"pk": self.nonprofit.pk}),
        )

        self.assertEqual(response.status_code, 200)

factories.py for Nonprofit, UserProfile, and CustomUser models

# accounts/tests/factories.py
class CustomUserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = CustomUser

    username = "username123"
    password = "password456"

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        manager = cls._get_manager(model_class)
        return manager.create_user(*args, **kwargs)


# profiles/tests/factories.py
class UserProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = UserProfile

    user = factory.SubFactory(CustomUserFactory)

    @factory.post_generation
    def following(self, create, extracted):
        if not create or not extracted:
            return
        self.following.add(*extracted)

# nonprofits/tests/factories.py
class NonprofitFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Nonprofit

    created_by = factory.SubFactory(UserProfileFactory)
    name = "Nonprofit Name"

accounts/models.py

class CustomUser(AbstractUser):
    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self.create_user_profile()

    def create_user_profile(self):
        from profiles.models import UserProfile

        UserProfile.objects.get_or_create(user=self)

terminal results of running tests

============================================================================================================= test session starts =============================================================================================================
platform darwin -- Python 3.10.7, pytest-8.2.0, pluggy-1.5.0
django: version: 5.0.6, settings: aa_django_project.settings (from ini)
rootdir: /Users/stephaniegoulet/Desktop/code/Hummingbird
configfile: pytest.ini
plugins: factoryboy-2.5.1, Faker-25.1.0, django-4.8.0
collected 2 items                                                                                                                                                                                                                             

nonprofits/tests/tests.py F.                                                                                                                                                                                                            [100%]

================================================================================================================== FAILURES ===================================================================================================================
_________________________________________________________________________________________ TestNonprofitViews.test_nonprofit_detailview_standard_user __________________________________________________________________________________________

self = <tests.TestNonprofitViews testMethod=test_nonprofit_detailview_standard_user>

    def test_nonprofit_detailview_standard_user(self):
        nonprofit = NonprofitFactory.build(name="XYZ Nonprofit")
    
        authenticated_standard_user = CustomUserFactory.create(
            username="authenticatedstandarduser",
            password="secret",
            is_staff=False,
            is_superuser=False,
        )
    
        self.client.login(username="authenticatedstandarduser", password="secret")
        response = self.client.get(
            reverse(
                "nonprofit_detail",
                kwargs={"pk": nonprofit.id},
            )
        )
    
>       self.assertEqual(response.status_code, 200)
E       AssertionError: 404 != 200

nonprofits/tests/tests.py:145: AssertionError
-------------------------------------------------------------------------------------------------------------- Captured log call --------------------------------------------------------------------------------------------------------------
WARNING  django.request:log.py:241 Not Found: /nonprofits/5cd9df72-ab56-48e8-91bd-e051b2523d53/
=========================================================================================================== short test summary info ===========================================================================================================
FAILED nonprofits/tests/tests.py::TestNonprofitViews::test_nonprofit_detailview_standard_user - AssertionError: 404 != 200
========================================================================================================= 1 failed, 1 passed in 2.64s =========================================================================================================```

I figured it out :partying_face: - posting solution below for reference. Two things:

  1. I switched build() for create() in test_nonprofit_detailview_standard_user
  2. Since the above resolved the original error, but there was then another error (IntegrityError: NOT NULL constraint failed: profiles_userprofile.user_id). I found a page in the factory-boy docs about django_get_or_create and edited the UserProfileFactory like this:
class UserProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = UserProfile
        django_get_or_create = ("user",)

    user = factory.SubFactory(CustomUserFactory)

    @factory.post_generation
    def following(self, create, extracted):
        if not create or not extracted:
            return
        self.following.add(*extracted)

PS: the test passes now, but there is now this deprecation warning:

DeprecationWarning: UserProfileFactory._after_postgeneration will stop saving the instance after postgeneration hooks in the next major release.
  If the save call is extraneous, set skip_postgeneration_save=True in the UserProfileFactory.Meta.
  To keep saving the instance, move the save call to your postgeneration hooks or override _after_postgeneration.
    warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html

I don’t fully understand this or how to adjust accordingly yet but that’s a separate problem