Search code examples
pythondjangofactory-boy

Django / FactoryBoy - Overriding lazy_attributes


I'm working on a Django project for which I've created the following factory:

class PurchaseFactory(factory.DjangoModelFactory):
    class Meta:
        model = 'core.Purchase'

    amount = decimal.Decimal(random.randrange(100, 100000) / 100)
    ( .... )
    user = factory.SubFactory(UserFactory)

    @factory.lazy_attribute
    def date(self):
        if not self.date:
            return timezone.now()
        else:
            return self.date

For which I'm running the following test:

class TestGetBalanceForPeriod(TestCase):
    def setUp(self) -> None:
        self.user: User = UserFactory()
        self.purchase_1: Purchase = PurchaseFactory(
            user=self.user, date=timezone.datetime(2019, 11, 1), amount=10
        )

    def test_test_setup(self) -> None:
        self.assertEqual(self.purchase_1.date, timezone.datetime(2019, 11, 1))

As you can see I've overridden the date field on the PurchaseFactory with an lazy_attribute. However, in this specific test I'm attempting to set the date myself. I imagined factoryboy would override all values with the values provided by the users, but the test fails with the following error:

AssertionError: datetime.datetime(2019, 11, 22, 16, 15, 56, 311882, tzinfo=<UTC>) != datetime.datetime(2019, 11, 1, 0, 0)

The first date is the results of the timezone.now() call and not the date that I provided as an input. So far I've been able to override all values on the many factories in our project without problems - but I'm unable to solve this problem.

Does anyone know what I am doing wrong?

edit

As requested I've included the code for the Purchase model below:

class Purchase(models.Model):
    amount = models.DecimalField(default=0, decimal_places=2, max_digits=8)
    ( .... )
    date = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(
        User,
        related_name='purchases',
        on_delete=models.CASCADE,
    )

Solution

  • The issue comes from using auto_now_add=True in your Django model definition: Django will override any provided value for date in that case, using the value from timezone.now().

    In order to opt out of that behaviour, use date = models.DateTimeField(default=timezone.now, editable=False) (or default=lambda: timezone.now(), easier to understand): it will default to using timezone.now(), but enables you to provide a custom value.

    NB: Setting editable=False keeps the default Django behaviour of preventing users from editing the field, which is often implied by automatically setting the value at insertion time.

    By the way, you don't need to check for if not self.date in your @lazy_attribute definition, FactoryBoy will use the provided value automatically. This allows you to use this shorter description:

    class PurchaseFactory(factory.django.DjangoModelFactory):
        ...
        date = factory.LazyFunction(timezone.now)