You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Using a factory to create an object (using .create()) will set args passed in onto the _original_params class attribute.
If a second factory uses that same factory as a SubFactory in another test case, the _original_params will still be set
to the values from the first test case because the SubFactory call does not reset the _original_params attribute.
To Reproduce
There's a working example below.
Model / Factory code
Check the code below for the factories and models.
The issue
This causes an issue when overriding the _create method in the factory to grab the _original_params since those will not
be valid/stale (especially because the SubFactory passes no params). When one of the _original_params is a model instance, on the subfactory call (in the separate test case) and is used on the _create override to set something in the object an integrity error
will be raised since the object referenced in the _original_params no longer exists in the database (it was deleted after the first
test was done).
fromdatetimeimporttimedeltafromunittestimportskipimportfactoryimportpytzfromdjango.testimportTestCasefromfactory.djangoimportDjangoModelFactoryfromappointments.modelsimportAppointment, AppointmentAttendance, AppointmentTypefrompeople.modelsimportPersonfromutils.testing_utilsimportCustomSubFactoryclassPersonFactory(DjangoModelFactory):
classMeta:
model=PersonclassAppointmentTypeFactory(DjangoModelFactory):
name=factory.Faker("company")
custom_id=factory.Faker("numerify", text="########")
classMeta:
model=AppointmentTypeclassAppointmentFactory(DjangoModelFactory):
appointment_type=factory.SubFactory(AppointmentTypeFactory)
start_at=factory.Faker("future_datetime", tzinfo=pytz.timezone("UTC"))
end_at=factory.LazyAttribute(lambdad: d.start_at+timedelta(hours=1))
@classmethoddef_create(cls, model_class, *args, **kwargs):
# NOTE:# Without the CustomSubFactory this params variable is still reflecting# values from the first use of the AppointmentFactory class. Notice that the# values are bound to the class and not to the instance.params=getattr(cls, "_original_params", {}) or {}
kwargs["appointment_type"] =cls._get_appointment_type(params)
returnsuper()._create(model_class, *args, **kwargs)
@classmethoddef_get_appointment_type(cls, params):
explicit=params.get("appointment_type", None)
ifisinstance(explicit, AppointmentType):
returnexplicitreturnAppointmentTypeFactory.create(name="Foo", custom_id="123456")
classMeta:
model=AppointmentclassAppointmentAttendanceFactory(DjangoModelFactory):
appointment=factory.SubFactory(AppointmentFactory)
member=factory.SubFactory(PersonFactory)
classMeta:
model=AppointmentAttendanceclassBetterAppointmentAttendanceFactory(DjangoModelFactory):
appointment=CustomSubFactory(AppointmentFactory)
member=factory.SubFactory(PersonFactory)
classMeta:
model=AppointmentAttendance# -------------------------------------------------------------------------------------classTestA(TestCase):
deftest_this_will_work_but_must_come_first(self):
type_=AppointmentTypeFactory.create(name="Bar")
# This usage is setting the original params in the AppointmentFactory class.# Those values get stuck there for the second use of this class.appointment=AppointmentFactory.create(appointment_type=type_)
assertappointment.appointment_type.name=="Bar"classTestB(TestCase):
@skip("This will fail because it's not using the CustomSubFactory")deftest_this_will_fail_if_second(self):
attendance=AppointmentAttendanceFactory.create()
# This errors with an InegrityError because the AppointmentFactory is trying to# use an AppointmentType that was crafted in the previous test. (note that we# can't assert the error because it occurs during the tear down.)assertisinstance(attendance.appointment.appointment_type.name, str)
deftest_this_will_work_if_second(self):
# The CustomSubFactory will reset the _original_params attribute when using the# SubFactory. This doens't happen with the default SubFactory.attendance=BetterAppointmentAttendanceFactory.create()
assertisinstance(attendance.appointment.appointment_type.name, str)
@mrwbarg and I have been working this issue together. Our subclass solution was merged yesterday and working fine in our code base with nearly 5K unittests and several dozen factories. We don't "need" a resolution for this but would be happy to make a PR here with our fix at the SubFactory.evaluate definition level. It might save another team 2 days of bug hunting.
Rather than just making a PR, we thought it was better to start with an issue. I know we are on the fringe here by extending the _create method and using _original_params in a way that might deviate from what was initially intended. However, by reading other issues here, it appears that we're not the only ones who have found an "off label" use for the _original_params classattr. I think it's reasonable to want a way to access the kwargs that were passed to the create or build function.
For that matter, things seem to have worked just fine until we ran into this asymmetric implementation of _create and create with regard to the SubFactory versus the DjangoModelFactory. Having the SubFactory class build using the _create function directly is a reasonable architectural choice (there are discussions in other posts on that choice) and it isn't the true surface area for this issue. In our opinion, the missing state reset on the _original_params attribute when _create is called directly by the SubFactory make this a bug rather than an architectural choice.
Description
Using a factory to create an object (using
.create()
) will set args passed in onto the_original_params
class attribute.If a second factory uses that same factory as a
SubFactory
in another test case, the_original_params
will still be setto the values from the first test case because the
SubFactory
call does not reset the_original_params
attribute.To Reproduce
There's a working example below.
Model / Factory code
Check the code below for the factories and models.
The issue
This causes an issue when overriding the
_create
method in the factory to grab the_original_params
since those will notbe valid/stale (especially because the SubFactory passes no params). When one of the
_original_params
is a model instance, on the subfactory call (in the separate test case) and is used on the_create
override to set something in the object an integrity errorwill be raised since the object referenced in the
_original_params
no longer exists in the database (it was deleted after the firsttest was done).
Notes
To fix this, we created the following class:
This resets the
_original_params
when the factory is called through as aSubFactory
Maybe another way to see it is as a
DjangoModelSubFactory
.The text was updated successfully, but these errors were encountered: