Search code examples
pythondjangopytestpytest-djangofactory-boy

Specify Factory db column names with FactoryBoy in a Django Test?


I've got a number of unmanaged models that I'm trying to develop some factories for so I can get some tests put together. The issue is that on a couple of them, they have db_column names and that is throwing an error for me in the factory.

My models look like this:


class Identity(models.Model):
    id = models.IntegerField(db_column="identityID", primary_key=True)
    person_id = models.IntegerField(db_column="personID")
    birth_date = models.DateField(db_column="birthdate")
    gender = models.CharField(db_column="gender", max_length=1)

    class Meta(object):
        # This is 'True' when testing so it's treated like a normal model
        managed = getattr(settings, "UNDER_TEST", False)
        db_table = "identity"

class Person(models.Model):
    id = models.IntegerField(db_column="personID", primary_key=True)
    identity = models.ForeignKey(
        Identity, db_column="currentIdentityID", on_delete=models.PROTECT
    )
    ticket_number = models.IntegerField(db_column="ticketNumber")

    class Meta(object):
        # This is 'True' when testing so it's treated like a normal model
        managed = getattr(settings, "UNDER_TEST", False)
        db_table = "person"

class YearTerm(models.Model):
    active = models.BooleanField(default=False)
    name = models.CharField(max_length=50)
    created_by = models.ForeignKey(
        Person, on_delete=models.PROTECT
    )

    class Meta:
        # This is 'True' when testing so it's treated like a normal model
        managed = getattr(settings, "UNDER_TEST", False)
        db_table = "[ALTS].[yearterm]"

My factories look like this:

class IdentityFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Identity

    @classmethod
    def _setup_next_sequence(cls):
        try:
            return Identity.objects.latest("id").id + 1
        except Identity.DoesNotExist:
            return 1

    id = factory.Sequence(lambda n: n)
    person_id = factory.Sequence(lambda n: n)
    birth_date = factory.fuzzy.FuzzyDateTime(timezone.now())
    gender = factory.Faker("random_element", elements=[x[0] for x in GENDERS])


class PersonFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Person

    @classmethod
    def _setup_next_sequence(cls):
        try:
            return Person.objects.latest("id").id + 1
        except Person.DoesNotExist:
            return 1

    id = factory.Sequence(lambda n: n)
    identity = factory.RelatedFactory(
        IdentityFactory,
        person_id=factory.SelfAttribute("..id"),
    )
    ticket_number = factory.Faker("random_int", min=1000, max=40000)

class YearTermFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = YearTerm
        django_get_or_create = ("name",)

    active = Iterator([True, False])
    name = FuzzyChoice(["Occasionally", "Sometimes", "Always"])
    created_by = SubFactory(PersonFactory)

My test case is extremely simple:

class TestCaseYearTerm(TestCase):
    def test_create(self):
        """
        Test the creation of a YearTerm model using a factory
        """
        year_term = YearTermFactory.create()
        self.assertEqual(YearTerm.objects.count(), 1)

But I get the following error:

django.db.utils.IntegrityError: NOT NULL constraint failed: person.currentIdentityID

I feel like this is because I specify the db_column name in the model, but I'm not sure how to fix that in FactoryBoy or to get factory boy to add a specific name to the factories attributes when creating them.


Solution

  • The models can be improved. OP has Person and Identity models and wants that one Person corresponds to only one Identity (like a User / Person Profile). OP can achieve that with a OneToOneField. So, one can edit OP's Identity model to

    class Identity(models.Model):
        person = models.OneToOneField(Person, on_delete=models.CASCADE)
    

    Note that I'm not using person_id. Using _id in Django Model Fields for OneToOneField and Foreign is an anti-pattern. Also, in OP's Person model, delete the identity field since it's not needed anymore. OP will need to run makemigrations after this.

    Then, OP's factories will get simpler

    import factory
    from factory import SubFactory
    from factory.django import DjangoModelFactory
    
    class IdentityFactory(DjangoModelFactory):
        person = SubFactory(PersonFactory)
         
        class Meta:
            model = Identity
    

    Don't forget to remove identity from PersonFactory as well.


    If OP wants to keep things as they are, without many model changes, something I would advise against (but then again it depends on the goals/context), then OP can just add database='special_db' to the factories Meta, like OP mentioned here.