Django, Pytest, Coverage, Factory Boy, pytest-cov and a bizarre breakpoint issue

written by Chris Goodwin on 2022-12-23

So you've got a test in your Django application that isn't quite working as expected.

Sees to be an issue with the User model and the full_name method.

class User(AbstractUser):
    password = models.CharField(max_length=128)
    last_login = models.DateTimeField(blank=True, null=True)
    is_superuser = models.BooleanField(default=False)
    username = models.CharField(unique=True, max_length=150)
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    email = models.CharField(max_length=254)
    phone = models.CharField(max_length=8, null=True, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    birth_date = models.DateField(null=True, blank=True)

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

You like ease of use - so you've got Factory Boy installed and have a simple UserFactory created:

class UserFactory(factory.django.DjangoModelFactory):
    """
    Factory for creating basic Users
    """

    password = "test"
    last_login = factory.LazyAttribute(
        lambda o: timezone.now() - timedelta(hours=1)
    )
    is_superuser = False
    username = factory.LazyAttributeSequence(
        lambda a, n: "{0}{1}{2}".format(a.first_name, a.last_name, n).lower()
    )
    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    email = factory.LazyAttributeSequence(
        lambda a, n: "{0}{1}{2}@your.org".format(
            a.first_name, a.last_name, n
        ).lower()
    )
    is_staff = False
    is_active = True
    phone = factory.Faker("numerify", text="####")

    class Meta:
        model = User
        django_get_or_create = ("username",)

    @factory.post_generation
    def groups(self, create, extracted, **kwargs):
        if not create or not extracted:
            # Simple build, or nothing to add, do nothing.
            return

        # Add the iterable of groups using bulk addition
        self.groups.add(*extracted)

No problem from here on out - you know the drill on finding these issues and get right into it. You hop into the test and throw in a breakpoint() where you think the issue is. You want to hop in the debugger poke around a little - maybe it's something simple.

It's a simple TestCase after all:

class TestCaseUser(TestCase):
    def test_full_name_method(self):
        """
        Test the full name method returns expected values
        """
        full_name = UserFactory.create(first_name="Bob", last_name="Roberts")
        breakpoint()
        self.assertEqual(full_name, "Bob Roberts")
        # Yes, this is oversimplified, to provide the example!

You run the test, the debugger opens and you crack your knuckles. This issue won't last long! You try to print out the variable that is causing the issue (print(full_name))- and the shell spits out that the variable is not defined.

--Call--
> /Users/chrisgoodwin/easy_project/simple_app/models.py(119)full_name()
-> @property
(Pdb) print(full_name)
*** NameError: name 'full_name' is not defined
(Pdb)

Wait, what?

You scratch your head and do the thing everyone in technology generally does: try it again (turn it off and back on, right?) Same results.

You see what the debugger is stating - but it is not supposed to work that way. It would appear that you're somehow dropped into the full_name function on the User model.

Your breakpoint is listed in the test; so your code should stop right there and toss you into the debugger - but it did not, the code kept executing until you hit another function - then it stopped executing and got you into the debugger.

It's bizarre, and it's weird. But it happened to me.

The reason? Apparently, there are some issues between the coverage library and the pytest-cov library. If you downgrade coverage enough versions - the issue goes away. Specifically downgrading to versions 6.4 and earlier eliminates the issue. Versions 6.4.1 and later cause this issue to occur. But downgrading that far? Eww.

I think there may be some unexpected complications from factory_boy library as well - because if you just manually create a User model using the Django provided methods (User.objects.create()) the issue does not occur in that case either. But seriously, giving up the functionality of Factory Boy, or downgrading multiple versions of the coverage library? No, thank you.

There is a simple solution (since the developers of some of these libraries are aware of the issues): run the tests with the additional --no-cov flag when you want the breakpoints to work as expected. There is a discussion in the PyCharm community about it because it affects the built-in debugger.

pytest simple_app/tests/test_models.py --no-cov

... then we're dumped into the debugger where we would expect and things work exactly as they should.

For the time being, until these libraries get the details of this little bugger under control (which at this point seems unlikely), running with --no-cov when we need to use breakpoints seems to suffice. It's a minor inconvenience, but at least there's a temporary solution!

For PyCharm users - you can add this to the debugging configuration pretty easily:

Happy Debugging!  

Did you find this article helpful?

Feel free to help me keep this blog going (with coffee)!