Как в FactoryBoy настроить фабрику с пустым полем члена «многие ко многим»? - PullRequest
3 голосов
/ 11 июля 2020

Я использую Django 3 с Python 3.8. У меня есть следующая модель ...

class Coop(models.Model):
    objects = CoopManager()
    name = models.CharField(max_length=250, null=False)
    types = models.ManyToManyField(CoopType, blank=False)
    addresses = models.ManyToManyField(Address)
    enabled = models.BooleanField(default=True, null=False)
    phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
    email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
    web_site = models.TextField()

Я создал следующую фабрику (используя Factory boy), чтобы попытаться создать модель в тесте ...

class CoopFactory(factory.DjangoModelFactory):
    """
        Define Coop Factory
    """
    class Meta:
        model = Coop

    name = "test model"
    enabled = True
    phone = factory.SubFactory(PhoneContactMethodFactory)
    email = factory.SubFactory(EmailContactMethodFactory)
    web_site = "http://www.hello.com"

    @factory.post_generation
    def addresses(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            # A list of types were passed in, use them
            for address in extracted:
                self.addresses.add(address)
        else:
            address = AddressFactory()
            self.addresses.add( address )

    @factory.post_generation
    def types(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            # A list of types were passed in, use them
            for type in extracted:
                self.types.add(type)
        else:
            print("Creating type ...\n")
            type = CoopTypeFactory()
            self.types.add( type )

но я У меня проблема с созданием фабрики с пустым полем (типами) "многие ко многим". Я пробовал следующее

@pytest.mark.django_db
def test_coop_create_with_no_types(self):
    """ Test customer model """    # create customer model instance
    coop = CoopFactory.create(types=[])
    print("size: ", coop.types.all().count())
    self.assertIsNotNone(coop)
    self.assertIsNotNone( coop.id )

, но значение types.all().count() всегда равно 1. Как правильно настроить фабрику с пустым полем многие-ко-многим?

Изменить: В ответ на полученный ответ, как правильно передать поле члена, которое будет использоваться фабрикой? Я пробовал

@pytest.mark.django_db
def test_coop_create_with_existing_type(self):
    """ Test customer model """    # create customer model instance
    coop_from_factory = CoopFactory()
    self.assertIsNotNone(coop_from_factory)

    coop_types = coop_from_factory.types
    coop = CoopFactory.create(types=[coop_types.all().first()], addresses=coop_from_factory.addresses.all())
    self.assertIsNotNone(coop)

, но получил эту ошибку, для строки «for _ in range (извлечено):» строка ...

Traceback (most recent call last):
  File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_models.py", line 48, in test_coop_create_with_existing_type
    coop = CoopFactory.create(types=coop_types, addresses=coop_from_factory.addresses.all())
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/factory/base.py", line 564, in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/factory/django.py", line 141, in _generate
    return super(DjangoModelFactory, cls)._generate(strategy, params)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/factory/base.py", line 501, in _generate
    return step.build()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/factory/builder.py", line 296, in build
    postgen_results[declaration_name] = declaration.declaration.call(
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/factory/declarations.py", line 622, in call
    return self.function(
  File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/factories.py", line 128, in types
    for _ in range(extracted):
TypeError: 'ManyRelatedManager' object cannot be interpreted as an integer

Ответы [ 2 ]

1 голос
/ 20 июля 2020

Исправление заключается в замене if extracted на if extracted is not None.

Объяснение

В Python пустой список является ложным 1 , но не None.

coop = CoopFactory.create(types=[])

Пустой список [] передается обработчику пост-генерации types в качестве параметра extracted.

@factory.post_generation
def types(self, create, extracted, **kwargs):
    if not create:
        # Simple build, do nothing.
        return

    if extracted:
        # A list of types were passed in, use them
        for type in extracted:
            self.types.add(type)
    else:
        print("Creating type ...\n")
        type = CoopTypeFactory()
        self.types.add( type )

Поскольку if extracted является проверкой истинности 1 , ложный пустой список попадает в блок else, где создается type. Итак, значение types.all().count() равно 1.

1 https://docs.python.org/3/library/stdtypes.html#truth -value-testing

0 голосов
/ 14 июля 2020

Пропустите

        else:
            print("Creating type ...\n")
            type = CoopTypeFactory()
            self.types.add( type )

, который всегда будет создавать CoopType по умолчанию.

Одна вещь, которая может быть не ясна из документации, - это @factory.post_generation хуки всегда звонил. Это означает, что операторы else во всех хуках post_generation в примере кода всегда будут вызываться.

Дополнительная информация: Простое отношение «многие ко многим»

Шаблон I использовать часто, если я хочу создать значения по умолчанию напрямую, это добавление функции к фабрике, которая в этом примере будет преобразована в:

@factory.post_generation
def create_types(self, create, extracted, **kwargs):
    if not create:
        # Simple build, do nothing.
        return

    if extracted:
        for _ in range(extracted):
            self.types.add(CoopTypeFactory())

Позволяет использовать CoopFactory(create_types=3).

Вот мой полный пример:

@factory.post_generation
def types(self, create, extracted, **kwargs):
    if not create:
        # Simple build, do nothing.
        return

    if extracted:
        # A list of types were passed in, use them
        for type in extracted:
            self.types.add(type)
    # Removed this because it always creates 1 CoopType as default and
    # it may not be the desired behaviour for all tests.
    # else:
    #     print("Creating type ...\n")
    #     type = CoopTypeFactory()
    #     self.types.add( type )

# Adding this function to have a simple way of just adding default CoopTypes
@factory.post_generation
def create_types(self, create, extracted, **kwargs):
    if not create:
        # Simple build, do nothing.
        return

    if extracted: # This must be an integer
        for _ in range(extracted):
            self.types.add(CoopTypeFactory())

Это дает дополнительные варианты использования:

CoopFactory(create_types=3) вызовет create_types и поместит int 3 в извлеченный параметр и создаст 3 CoopTypes по умолчанию. (Это обеспечивает простое использование)

CoopFactory(types=[CoopTypeFactory()]) вызовет типы и поместит список из 1 CoopType в извлеченный параметр. (Это дает больший контроль над созданием CoopTypes, если этим объектам требуются некоторые c значения)

...