2015-10-12 6 views
6

Sto usando factory.LazyAttribute all'interno di una chiamata SubFactory di passare in un oggetto, creata nel factory_parent. Funziona benePassare un oggetto creato con SubFactory e LazyAttribute ad un RelatedFactory in factory_boy

Ma se si passa l'oggetto creato a RelatedFactory, LazyAttribute non è più possibile vedere factory_parent e non riesce.

Questo funziona bene:

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)) 

La chiamata identico a LazyAttribute fallisce qui:

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) 

Il identico LazyAttribute chiamata può più vedere factory_parent, e può accedere solo AnotherObject valori. LazyAttribute genera l'errore:

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

C'è un modo per aggirare questo?

Non posso mettere sub_object = sub_object nella chiamata ObjectFactory, vale a dire:

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

perché se lo faccio poi:

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

viene creato un secondo sub_object, mentre ho bisogno di entrambi oggetti per riferirsi allo stesso sub_object. Ho provato SelfAttribute inutilmente.

+0

Penso che se specifichi lo stesso pk, risolverà il problema. –

+0

Vuoi dire "sub_object_id = sub_object_id'? O 'LazyAttribute (lambda obj: obj.factory_parent.sub_object.id)'? – Chris

+0

Le risposte a [questa domanda] (http://stackoverflow.com/questions/32995158/getting-id-of-associated-child-records-in-factory-boy/33043428#33043428) sembrano indicare un approccio alternativo, ma non riesco a farlo per soddisfare questo requisito. (Questa domanda è per correggere un bug in una di quelle risposte.) – Chris

risposta

4

Penso che si possa sfruttare la possibilità di sovrascrivere i parametri passati a RelatedFactory per ottenere ciò che si desidera.

Ad esempio, dato:

class MyFactory(OKFactory): 

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

Se sapevamo cosa il valore di object stava per essere in anticipo, potremmo farlo funzionare con qualcosa di simile:

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

Possiamo usare questa stessa convenzione di denominazione per passare l'oggetto allo RelatedFactory all'interno dello Factory principale:

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'} 
2

Ho risolto questo problema semplicemente chiamando le fabbriche entro @factory.post_generation. A rigor di termini questa non è una soluzione al problema specifico posto, ma spiego di seguito in grande dettaglio perché questa risultò essere un'architettura migliore. @ La soluzione di rhunwick passa autenticamente da SubFactory(LazyAttribute('')) a RelatedFactory, tuttavia restavano delle restrizioni che significavano che non era giusto per la mia situazione.

spostiamo la creazione di sub_object e objectProblemFactory-ObjectWithSubObjectsFactory (e rimuovere la clausola exclude), e aggiungere il seguente codice alla fine del 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() 

così sembra RelatedFactory chiamate vengono eseguite prima PostGeneration chiamate.

La denominazione in this question è più facile da capire, ecco lo stesso codice di soluzione per questo problema del campione:

La creazione di dataset, column_1 e column_2 vengono spostati in una nuova fabbrica DatasetAnd2ColumnsFactory, e il codice qui sotto è quindi aggiunto alla fine di 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() 

Ho poi esteso questo approccio passando opzioni per configurare la fabbrica, in questo modo:

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

Allora questo codice di fabbrica rilevato le opzioni e ha generato i dati di test richiesti (notare il metodo viene rinominato options per abbinare il prefisso sui nomi dei parametri):

@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' 

ho poi ulteriormente esteso questo. Poiché i miei modelli desiderati contenevano auto join, la mia fabbrica è ricorsiva. Così, per una chiamata come ad esempio:

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

Entro @factory.post_generation ho:

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 

Alcune note non è necessario leggere il motivo per cui sono andato con questa opzione invece di @ corretta soluzione di rhunwicks alla mia domanda di cui sopra. C'erano due ragioni.

La cosa che mi ha impedito di sperimentarlo era che l'ordine di RelatedFactory e di post-generazione non è affidabile: i fattori apparentemente non correlati lo influenzano, presumibilmente una conseguenza della valutazione pigra. Avevo degli errori in cui un insieme di fabbriche avrebbe improvvisamente smesso di funzionare senza una ragione apparente. Una volta era perché ho rinominato le variabili a cui RelatedFactory era assegnato. Sembra ridicolo, ma l'ho testato fino alla morte (e pubblicato here) ma non c'è dubbio: la modifica delle variabili ha commutato in modo affidabile la sequenza di RelatedFactory e l'esecuzione post-gen. Ho sempre pensato che si trattasse di una supervisione a mio nome fino a quando non è successo di nuovo per qualche altro motivo (che non sono mai riuscito a diagnosticare).

In secondo luogo ho trovato il codice dichiarativo confuso, inflessibile e difficile da ri-fattore. Non è semplice passare diverse configurazioni durante l'istanziazione in modo che lo stesso factory possa essere utilizzato per diverse variazioni dei dati di test, ovvero ho dovuto ripetere il codice, object deve essere aggiunto a un elenco di Factory Meta.exclude - sembra banale ma quando hai pagine di codice che genera dati è stato un errore affidabile. Come sviluppatore dovresti passare diverse fabbriche diverse volte per capire il flusso di controllo. Il codice di generazione sarebbe stato distribuito tra il corpo dichiarativo, fino a quando non avresti esaurito questi trucchi, poi il resto sarebbe andato in post-generazione o sarebbe diventato molto contorto. Un esempio comune per me è una triade di modelli interdipendenti (ad esempio, una struttura di categoria genitore-figlio o dataset/attributi/entità) come chiave esterna di un'altra triade di oggetti interdipendenti (ad es. Modelli, valori dei parametri, ecc. ai valori dei parametri di altri modelli). Alcuni di questi tipi di strutture, soprattutto se nidificate, diventano rapidamente ingestibili.

Mi rendo conto che non è proprio nello spirito di factory_boy, ma mettere tutto in post-generazione ha risolto tutti questi problemi. Posso passare i parametri, quindi la stessa singola fabbrica serve tutti i miei requisiti di dati di test del modello composito e nessun codice viene ripetuto. La sequenza di creazione è facile da vedere immediatamente, ovvia e completamente affidabile, piuttosto che dipendere da catene confuse di ereditarietà e di sovrapposizione e soggette a qualche bug. Le interazioni sono ovvie quindi non è necessario digerire tutto per aggiungere funzionalità, e le diverse aree di funzionalità sono raggruppate nelle clausole if post generazione. Non è necessario escludere variabili di lavoro e puoi farvi riferimento per la durata del codice di fabbrica. Il codice di test unitario è semplificato, in quanto la descrizione della funzionalità viene eseguita nei nomi dei parametri anziché in quelli della classe Factory, pertanto è possibile creare dati con una chiamata come WhateverFactory(options__create_xyz=True, options__create_abc=True.. anziché WhateverCreateXYZCreateABC..(). Ciò rende abbastanza chiara la codifica delle responsabilità.

Problemi correlati