Search code examples
pythondjangodjango-testingfactory-boy

Factory-boy / Django - Factory instance not reflecting changes to model instance


I am writing tests for a website I'm working on and I'm representing the models with factoryboy Factory objects.

However, I've run into some behavior I found somewhat confusing and I was wondering if anyone here would be so kind to explain it to me

I'm running a test that contains the following model:

STATUS = (
    ('CALCULATING'),
    ('PENDING'),
    ('BUSY'),
    ('SUCCESS'),
    ('FAILED')
)


class SchoolImport(models.Model):
    date = models.DateTimeField(auto_now_add=True)
    status = models.CharField(
        verbose_name=_('status'), choices=STATUS,
        max_length=50, default='CALCULATING'
    )

For which I've created the following factory. As you can see the status is set to its default value, which I found more realistic than having a randomly selected value

class SchoolImportFactory(factory.DjangoModelFactory):
    class Meta:
        model = models.SchoolImport

    status = 'CALCULATING'
    school = factory.SubFactory(SchoolFactory)

    @factory.lazy_attribute
    def date(self):
        return timezone.now() - datetime.timedelta(days=10)

Below you'll see both a (simplified) version of the function that is being tested, as well as the test itself. (I've currently commented out all other code on my laptop, so the function that you see below is an accurate representation)

The gist of it is that the function receives an id value that it will use to fetch an SchoolImport object from the database and change its status. The function will be run in celery and thus returns nothing.

When I run this test through the debugger I can see that the value is changed correctly. However, when the test runs its final assertion it fails as self.school_import.status is still equal to CALCULATING.


#app.utils.py
def process_template_objects(school_import_pk):
    school_import = models.SchoolImport.objects.get(id=import_file_pk)
    school_import.status = 'BUSY'
    school_import.save()



#app.tests.test_utils.py
class Test_process_template_objects_function(TestCase):

    def setUp(self):
        self.school = SchoolFactory()
        self.school_import = SchoolImportFactory(
            school=self.school
        )

    def test_function_alters_school_import_status(self):
        self.assertEqual(
            self.school_import.status, 'CALCULATING'
        )
        utils.process_template_objects(self.school_import.id)
        self.assertNotEqual(
            self.school_import.status, 'CALCULATING'
        )

When I run this test through a debugger (with a breakpoint just before the failing assertion) and run SchoolImport.objects.get(id=self.school_import.id).status it does return the correct BUSY value.

So though the object being represented by the FactoryInstance is being updated correctly, the changes are not reflected in the factory instance itself.

Though I realize I'm probably doing something wrong here / encountering expected behavior, I was wondering how people who write tests using factoryboy fget around this behavior, or if perhaps there was a way to 'refresh' the factoryboy instance to reflect changes to the model instance.


Solution

  • The issue comes from the fact that, in process_template_objects, you work with a different instance of the SchoolImport object than the one in the test.

    If you run:

    a = models.SchoolImport.objects.get(pk=1)
    b = models.SchoolImport.objects.get(pk=2)
    
    assert a == b  # True: both refer to the same object in the database
    assert a is b  # False: different Python objects, each with its own memory
    
    a.status = 'SUCCESS'
    a.save()
    assert a.status == 'SUCCESS'  # True: it was indeed changed in this Python object
    assert b.status == 'SUCCESS'  # False: the 'b' object hasn't seen the change
    

    In order to fix this, you should refetch the instance from the database after calling process_template_objects:

    utils.process_template_objects(self.school_import.id)
    self.school_import.refresh_from_db()
    

    See https://docs.djangoproject.com/en/2.2/ref/models/instances/#refreshing-objects-from-database for a more detailed explanation!