Passing parameters through factory-boy Factory to SubFactory without specifying it

3.8k Views Asked by At

I'm using the pythons factory_boy package to create instances of models for testing purposes.

I want to pass the parameters used when calling Facotry.create() to all the SubFactories in the Factory being called.

Here's how I do it now:

Example 1: I have to explicitly set the company when calling the SubFactory (the BagFactory)

class BagTrackerFactory(BaseFactory):
    company = factory.SubFactory(CompanyFactory)
    bag = factory.SubFactory(BagFactory, company=factory.SelfAttribute("..company"))


class BagFactory(BaseFactory):
    company = factory.SubFactory(CompanyFactory)

Example 2: In this example, I have to add company to Params in the BagFactory, so I can pass it down to ItemFactory which has the company parameter.

class BagTrackerFactory(BaseFactory):
    company = factory.SubFactory(CompanyFactory)
    bag = factory.SubFactory(BagFactory, company=factory.SelfAttribute("..company"))


class BagFactory(BaseFactory):
    item = factory.SubFactory(ItemFactory, company=factory.SelfAttribute("..company"))

    class Params:
        company = factory.SubFactory(CompanyFactory)


class ItemFactory(BaseFactory):
    company = factory.SubFactory(CompanyFactory)

The reason why I do it like this is that it saves time and it makes sense that the Bag belongs to the same company as the BagTracker when created by the same Factory.

Note: the BaseFactory is factory.alchemy.SQLAlchemyModelFactory

Question:

What I would like is to have company (and all the other parameters) from the parent Factory be passed down to SubFactories without having to pass it explicitly. And this continues downstream all the way to the last SubFactory, so every model has the same company, from the topmost parent Factory to the lowest child SubFactory. I hope you understand what I'm saying.

Is there an easy way to do this? Like some option in the factory-boy package?

EDIT:

I ended up doing it the long way, passing down parameters manually. In this example, I'm showing both cases: when the parent factory has the company parameter(BagTrackerFactory) and doesn't have it but must pass it downstream (BagFactory).

class CompanyFactory(BaseFactory):
    id = get_sequence()

    class Meta:
        model = Company


class ItemFactory(BaseFactory):
    id = get_sequence()
    owner = factory.SubFactory(CompanyFactory)
    owner_id = factory.SelfAttribute("owner.id")

    class Meta:
        model = Item


class BagFactory(BaseFactory):
    id = get_sequence()
    item = factory.SubFactory(ItemFactory, owner=factory.SelfAttribute("..company"))
    item_id = factory.SelfAttribute("item.id")

    class Params:
        company = factory.SubFactory(CompanyFactory)

    class Meta:
        model = Bag


class BagTrackerFactory(BaseFactory):
    id = get_sequence()
    company = factory.SubFactory(CompanyFactory)
    company_id = factory.SelfAttribute("company.id")
    item = factory.SubFactory(ItemFactory, owner=factory.SelfAttribute("..company"))
    item_id = factory.SelfAttribute("item.id")
    bag = factory.SubFactory(BagFactory, company=factory.SelfAttribute("..company"))
    bag_id = factory.SelfAttribute("bag.id")

    class Meta:
        model = BagTracker

1

There are 1 best solutions below

2
On

This is possible, but will have to be done specifically for your codebase.

At its core, a factory has no knowledge of your models' specific structure, hence can't forward the company field — for instance, some models might not accept that field in their __init__, and providing the field would crash.

However, if you've got a chain where the field is always accepted, you may use the following pattern:

class WithCompanyFactory(factory.BaseFactory):
  class Meta:
    abstract = True

  company = factory.Maybe(
    "factory_parent",  # Is there a parent factory?
    yes_declaration=factory.SelfAttribute("..company"),
    no_declaration=factory.SubFactory(CompanyFactory),
  )

This works thanks to the factory_parent attribute of the stub used when building a factory's parameters: https://factoryboy.readthedocs.io/en/stable/reference.html#parents This field either points to the parent (when the current factory is called as a SubFactory), or to None. With a factory.Maybe, we can copy the value through a factory.SelfAttribue when a parent is defined, and instantiate a new value.

This can be used afterwards in your code:

class ItemFactory(WithCompanyFactory):
  pass

class BagFactory(WithCompanyFactory):
  item = factory.SubFactory(ItemFactory)

class BagTrackerFactory(WithCompanyFactory):
  bag = factory.SubFactory(BagFactory)


>>> tracker = BagTrackerFactory()
>>> assert tracker.company == tracker.bag.company == tracker.bag.item.company
... True

# It also works starting anywhere in the chain:
>>> company = Company(...)
>>> bag = BagFactory(company=company)
>>> assert bag.company == bag.item.company == company
... True

If some models must pass the company value to their subfactories, but without a company field themselves, you may also split the special WithCompanyFactory into two classes: WithCompanyFieldFactory and CompanyPassThroughFactory:

class WithCompanyFieldFactory(factory.Factory):
  """Automatically fill this model's `company` from the parent factory."""
  class Meta:
    abstract = True

  company = factory.Maybe(
    "factory_parent",  # Is there a parent factory?
    yes_declaration=factory.SelfAttribute("..company"),
    no_declaration=factory.SubFactory(CompanyFactory),
  )


class CompanyPassThroughFactory(factory.Factory):
  """Expose the parent model's `company` field to subfactories declared in this factory."""
  class Meta:
    abstract = True

  class Params:
    company = factory.Maybe(
      "factory_parent",  # Is there a parent factory?
      yes_declaration=factory.SelfAttribute("..company"),
      no_declaration=factory.SubFactory(CompanyFactory),
    )