Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auto_now_add overrides factoryboy params #102

Closed
boxed opened this issue Oct 31, 2013 · 8 comments
Closed

auto_now_add overrides factoryboy params #102

boxed opened this issue Oct 31, 2013 · 8 comments
Labels

Comments

@boxed
Copy link

boxed commented Oct 31, 2013

Say I have a model:

class Foo(models.Model):
    foo = models.DateField(auto_now_add=True)

with a trivial factory FooFactory, if I then do:

print FooFactory(foo=date(2001, 1, 1)).foo

it prints the current date, not the one I specified. To me this is a violation of principle of least surprise.

@rbarrois
Copy link
Member

rbarrois commented Nov 7, 2013

Hi,

That's a built-in "feature" of Django: when a field has auto_now_add=True, Django will override any provided value with django.utils.timezone.now() ; this happens ''after'' factory_boy's code runs.

You have two options:

  1. Override your factory' _create function:
class FooFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = models.Foo

    @classmethod
    def _create(cls, target_class, *args, **kwargs)
        foo = kwargs.pop('foo', None)
        obj = super(FooFactory, cls)._create(target_class, *args, **kwargs)
        if foo is not None:
            obj.foo = foo
            obj.save()
        return obj
  1. Replace auto_now_add=True with the default keyword argument to models:
from django.utils import timezone

class Foo(models.Model):
    foo = models.DateField(default=timezone.now)

Note the lack of brackets in default=timezone.now; this could also be written as default=lambda: timezone.now() to be more explicit.

I have personnally chosen the second version, which lets me override created_at fields easily in tests.

thibault added a commit to Talengi/phase that referenced this issue Jan 27, 2014
Replacing auto_now_add with default=timezone.now makes it possible to
override the field value in tests.

See FactoryBoy/factory_boy#102
@l-astro
Copy link

l-astro commented Oct 11, 2018

Quick question - how should this be implemented for auto_add (rather than auto_add_now)? I have an "updated" field, but whether I use models.DateTimeField(auto_now=True) or override the django model .save() method, I can't seem to get FactoryBoy to return the correct value of updated (LazyAttribute(lambda o: o.created + timedelta(hours=1)) or using the _create() method) - it always returns timezone.now().

What's the best way to go here?

@rbarrois
Copy link
Member

@lmannering I'm not sure about the issue you're having?

Basically:

  • If you set auto_now on a Django field, the current value of the field will be overwritten when calling .save()
  • Since that operation happens during the .save() step, its result cannot be seen in the factory (which operates before any instance of the model exists).

In other words, the way thinks work are:

  1. factory_boy resolves all declarations into a set of values;
  2. Those values are passed to MyModel.objects.create()
  3. During the actual .save() from Django's core, the value of updated is replaced with timezone.now()

@l-astro
Copy link

l-astro commented Oct 15, 2018

Hi @rbarrois my fault, I wasn't particularly clear - my question is, given your two options on how to best circumvent auto_add_now when using FactoryBoy, what's the best way to fake an "updated" model field (that has either auto_add=True set on the model field or a .save() method with timezone.now), given that order of operations?

I've currently monkeypatched the .save() method on the original model before passing to FactoryBoy, is that the best way to do it?

@boxed
Copy link
Author

boxed commented Oct 15, 2018

@lmannering maybe the solution is to use freezegun and circumvent this and other problems all together?

sergeyklay added a commit to sergeyklay/branch that referenced this issue Apr 18, 2021
This makes it possible to override the field value in tests.

Refs:
  - FactoryBoy/factory_boy#102
@pymen
Copy link

pymen commented Dec 15, 2021

Here is my way to do it in a universal way with freeze gun https://stackoverflow.com/a/70364787/1731460

@ashirazi2
Copy link

That's a built-in "feature" of Django: when a field has auto_now_add=True, Django will override any provided value with django.utils.timezone.now() ; this happens ''after'' factory_boy's code runs.

You have two options:

  1. Override your factory' _create function:
  2. Replace auto_now_add=True with the default keyword argument to models:

I know this is an old thread, but there is a third method that I use; override the date without saving…

class Foo(models.Model):
    created_at = models.DateField(auto_now_add=True)

foo = FooFactory.create()
foo.created_at = date(2001, 1, 1))
# use foo in assertion

@krystofbe
Copy link

That's a built-in "feature" of Django: when a field has auto_now_add=True, Django will override any provided value with django.utils.timezone.now() ; this happens ''after'' factory_boy's code runs.
You have two options:

  1. Override your factory' _create function:
  2. Replace auto_now_add=True with the default keyword argument to models:

I know this is an old thread, but there is a third method that I use; override the date without saving…

class Foo(models.Model):
    created_at = models.DateField(auto_now_add=True)

foo = FooFactory.create()
foo.created_at = date(2001, 1, 1))
# use foo in assertion

This was quite helpful. In addition to your approach, I found it necessary to include the .save() method to ensure the changes were persisted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants