Passing an object created using SubFactory and LazyAttribute to LinkedFactory in factory_boy

I am using factory.LazyAttribute in a SubFactory call to pass an object created in factory_parent . It works great.

But if I pass the object created in RelatedFactory , LazyAttribute can no longer see factory_parent and fails.

This works great:

 class OKFactory(factory.DjangoModelFactory): class = Meta: model = Foo exclude = ['sub_object'] sub_object = factory.SubFactory(SubObjectFactory) object = factory.SubFactory(ObjectFactory, sub_object=factory.LazyAttribute(lambda obj: obj.factory_parent.sub_object)) 

The identical LazyAttribute call is not made LazyAttribute :

 class ProblemFactory(OKFactory): class = Meta: model = Foo exclude = ['sub_object', 'object'] sub_object = factory.SubFactory(SubObjectFactory) object = factory.SubFactory(ObjectFactory, sub_object=factory.LazyAttribute(lambda obj: obj.factory_parent.sub_object)) another_object = factory.RelatedFactory(AnotherObjectFactory, 'foo', object=object) 

The exact LazyAttribute call LazyAttribute no longer see factory_parent and is only available for AnotherObject values. LazyAttribute throws an error:

 AttributeError: The parameter sub_object is unknown. Evaluated attributes are...[then lists all attributes of AnotherObjectFactory] 

Is there any way around this?

I cannot just put sub_object = sub_object in an ObjectFactory call, i.e.:

  sub_object = factory.SubFactory(SubObjectFactory) object = factory.SubFactory(ObjectFactory, sub_object=sub_object) 

because if I then do:

  object2 = factory.SubFactory(ObjectFactory, sub_object=sub_object) 

a second sub_object is created, whereas I need both objects to refer to the same sub_object. I tried SelfAttribute no avail.

+6
source share
2 answers

I think you can use the ability to override the parameters passed to RelatedFactory to achieve what you want.

For example, given:

 class MyFactory(OKFactory): object = factory.SubFactory(MyOtherFactory) related = factory.RelatedFactory(YetAnotherFactory) # We want to pass object in here 

If we knew what the value of object would mean, we could make it work with something like:

 object = MyOtherFactory() thing = MyFactory(object=object, related__param=object) 

We can use the same naming convention to pass an object to a RelatedFactory within the main Factory :

 class MyFactory(OKFactory): class Meta: exclude = ['object'] object = factory.SubFactory(MyOtherFactory) related__param = factory.SelfAttribute('object') related__otherrelated__param = factory.LazyAttribute(lambda myobject: 'admin%d_%d' % (myobject.level, myobject.level - 1)) related = factory.RelatedFactory(YetAnotherFactory) # Will be called with {'param': object, 'otherrelated__param: 'admin1_2'} 
+4
source

I solved this by simply calling the factories at @factory.post_generation . Strictly speaking, this is not a solution to a specific given problem, but I will explain in detail below why this turned out to be the best architecture. The @rhunwick solution does indeed pass SubFactory(LazyAttribute('')) to RelatedFactory , however the restrictions remained, which meant that it was wrong for my situation.

We move the creation of sub_object and object from ProblemFactory to ObjectWithSubObjectsFactory (and delete the exclude clause) and add the following code to the end of ProblemFactory .

 @factory.post_generation def post(self, create, extracted, **kwargs): if not create: return # No IDs, so wouldn't work anyway object = ObjectWithSubObjectsFactory() sub_object_ids_by_code = dict((sbj.name, sbj.id) for sbj in object.subobject_set.all()) # self is the `Foo` Django object just created by the `ProblemFactory` that contains this code. for another_obj in self.anotherobject_set.all(): if another_obj.name == 'age_in': another_obj.attribute_id = sub_object_ids_by_code['Age'] another_obj.save() elif another_obj.name == 'income_in': another_obj.attribute_id = sub_object_ids_by_code['Income'] another_obj.save() 

So, it seems that RelatedFactory calls RelatedFactory made before PostGeneration calls.

Naming in this question is easier to understand, so here is the same solution code for this problem:

Creating dataset , column_1 and column_2 moved to the new factory DatasetAnd2ColumnsFactory , and then the code below is added to the end of the FunctionToParameterSettingsFactory .

 @factory.post_generation def post(self, create, extracted, **kwargs): if not create: return dataset = DatasetAnd2ColumnsFactory() column_ids_by_name = dict((column.name, column.id) for column in dataset.column_set.all()) # self is the `FunctionInstantiation` Django object just created by the `FunctionToParameterSettingsFactory` that contains this code. for parameter_setting in self.parametersetting_set.all(): if parameter_setting.name == 'age_in': parameter_setting.column_id = column_ids_by_name['Age'] parameter_setting.save() elif parameter_setting.name == 'income_in': parameter_setting.column_id = column_ids_by_name['Income'] parameter_setting.save() 

Then I expanded this approach by passing parameters for factory settings, for example:

 whatever = WhateverFactory(options__an_option=True, options__another_option=True) 

Then this factory code detected the parameters and generated the required test data (note that the method is renamed to options to match the parameter name prefix):

 @factory.post_generation def options(self, create, not_used, **kwargs): # The standard code as above if kwargs.get('an_option', None): # code for custom option 'an_option' if kwargs.get('another_option', None): # code for custom option 'another_option' 

Then I expanded it. Since my desired models contained self, my factory is recursive. Therefore, for a call such as:

 whatever = WhateverFactory(options__an_option='xyz', options__an_option_for_a_nested_whatever='abc') 

Inside @factory.post_generation I have:

 class Meta: model = Whatever # self is the top level object being generated @factory.post_generation def options(self, create, not_used, **kwargs): # This generates the nested object nested_object = WhateverFactory( options__an_option=kwargs.get('an_option_for_a_nested_whatever', None)) # then join nested_object to self via the self join self.nested_whatever_id = nested_object.id 

Some notes you don't need to read about why I went with this option and not with the correct @rhunwicks solution on my question above. There were two reasons.

The thing that prevented me from experimenting with it was that the order of RelatedFactory and post-generation is not reliable - it seems to be affected by unrelated factors, apparently the result of a lazy assessment. I had errors when many factories suddenly stopped working for no apparent reason. This was once because I renamed the variables that were assigned. This sounds ridiculous, but I tested it to death (and posted it here ), but there is no doubt - renaming the variables reliably switched the sequence of work with ShareFactory and post-gen. I still thought it was some kind of supervision on my behalf until it happened again for some other reason (which I never managed to diagnose).

Secondly, I found the declarative code confusing, inflexible, and difficult to override. It’s not easy to pass different configurations at the time of creating the instance, so the same factory can be used for different types of test data, that is, I had to repeat the code, object needs to be added to the factory list Meta.exclude sounds trivial, but when you have pages data generating code, it was a reliable error. As a developer, you will have to go to several plants several times to understand the flow of control. The generation code will be distributed between the declarative body until you have exhausted these tricks, and the rest will remain alone generation or very confused. A common example for me is a triad of interdependent models (for example, the structure of categories of parents or children or a set of data / attributes / entities) as a foreign key of another triad of interdependent objects (for example, models, parameter values, etc., referring to the parameter values ​​of others models). Some of these types of structures, especially nested ones, quickly become unmanageable.

I understand that in fact this is not in the spirit of factory_boy, but everything in the post-generation solves all these problems. I can pass parameters, so the same single factory serves all my requirements for the test data of the composite model, and the code does not repeat. The creation sequence is easy to see immediately, obvious and completely reliable, and not depending on the confusion of the inheritance and redefinition chains and exposure to some error. The interactions are obvious, so you don’t have to digest everything to add some functionality, and the various areas of funtionality are grouped in post-generation if statements. There is no need to exclude working variables, and you can refer to them throughout the life of the factory code. The unit test code is simplified, because the description of the functions occurs in the parameter names, and not in the factory class names, so you create data with a call of type WhateverFactory(options__create_xyz=True, options__create_abc=True.. rather than WhateverCreateXYZCreateABC..() . This does a good separation of duties is pretty clean for the code.

+2
source

All Articles