25

When a field in a Django model has the option choices, see Django choices field option, it utilises an iterable containing iterables of 2 items to define which values are allowed. For example:

Models

class IceCreamProduct(models.Model):
    PRODUCT_TYPES = (
        (0, 'Soft Ice Cream'),
        (1, 'Hard Ice Cream'),
        (2, 'Light Ice Cream'),
        (3, 'French Ice Cream'),
        (4, 'Italian-style Gelato'),
        (5, 'Frozen Dairy Dessert'),
    )
    type = models.PositiveSmallIntegerField('Type', choices=PRODUCT_TYPES, default=0)

To generate a random value in Factory Boy for choices I would utilise factory.fuzzy.FuzzyChoice, but this only chooses an iterable of 2 items. It can not take the first item of the chosen iterable. For example:

Factories

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = factory.fuzzy.FuzzyChoice(IceCreamProduct.PRODUCT_TYPES)

Error

TypeError: int() argument must be a string, a bytes-like object or a number, not 'tuple'

Getting the first item of the tuple is not possible. For example:

Factories

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = factory.fuzzy.FuzzyChoice(IceCreamProduct.PRODUCT_TYPES)[0]

Error

TypeError: 'FuzzyChoice' object does not support indexing

It is possible with the default Python random iterator, but this generates a value on declaration time and so every factory object will have the same random value. For example:

Factories

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = random.choice(IceCreamProduct.PRODUCT_TYPES)][0]

How can this be solved in Factory Boy? Do I need to create a custom FuzzyAttribute? (If so, please give an example)

Robin
  • 519
  • 1
  • 7
  • 13
  • Hi, Robin and All just got a new similar question, how factory boy behave, if the the choice attribute is not covered in the factory. will it automatically select one in the choices? I did not get this answer in factory boy's document. Thanks – tim Feb 01 '19 at 03:30
  • @tim If you do not define the field (which has the choices attribute) in the factory, then it will take the value of the "default" attribute of that field. Are you getting an error or something? Because you should just be able to try it. – Robin Feb 04 '19 at 10:00
  • my case is that there is a test case I wrote, occasionally failed in pipeline, but always passing when running locally, I want to know the reason, I find one potential issue might be choice selecting... – tim Feb 05 '19 at 20:04
  • @tim I think I might have had similar "random failure" problems before with factories. You might want to check how you create the factory: you can use "ice_cream = IceCreamProductFactory() ice_cream.save()" or you can use "ice_cream = IceCreamProductFactory.create()", the last of which seems to have been working more reliably in my case. Also, did you try adding the choice attribute and seeing if it was more stable? – Robin Feb 07 '19 at 08:47

5 Answers5

35

You'll not need a FuzzyAttribute.

You can either restrict the values possible and only give the int value of each product type to FuzzyChoice by doing something like this:

PRODUCT_IDS = [x[0] for x in IceCreamProduct.PRODUCT_TYPES]
class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = factory.fuzzy.FuzzyChoice(PRODUCT_IDS)

It should do the work.

Please be aware that fuzzy module has been deprecated recently, see ( https://factoryboy.readthedocs.org/en/latest/fuzzy.html), you may want to use a LazyFunction instead.

Boris Feld
  • 825
  • 7
  • 8
10

You can do as easy as this

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    icecream_flavour = factory.Faker(
        'random_element', elements=[x[0] for x in IceCreamProduct.PRODUCT_TYPES]
    )

    class Meta:
        model = IceCreamProduct

PS. Don't use type as attribute, it is a bad practice to use a built-in function name as an attribute

Artem Bernatskyi
  • 4,185
  • 2
  • 26
  • 35
  • Does not work in my case (yet): raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0]) – Nrzonline Mar 19 '18 at 19:21
  • Please view my topic related to this error on [Django Factoryboy fill modelfield with choices throws error](https://stackoverflow.com/questions/49371682/django-factoryboy-fill-modelfield-with-choices-throws-error). – Nrzonline Mar 20 '18 at 13:56
  • @ArtemBernatskyi According PS. What we should use instead of `type` field ? Any alternatives ? – R. García Sep 01 '20 at 21:08
  • 1
    @R.García , I've updated the answer, speaking of alternatives to using `type` we can use any synonym which suits our context for example category/flavour/sort. – Artem Bernatskyi Sep 02 '20 at 18:23
9

Here is how I was able to do it using factory.LazyFunction as lothiraldan suggested:

import random

...


def get_license_type():
    "Return a random license type from available choices."
    lt_choices = [x[0] for x in choices.LICENSE_TYPE_CHOICES]
    return random.choice(lt_choices)


def get_line_type():
    "Return a random line type from available choices."
    lt_choices = [x[0] for x in choices.LINE_TYPE_CHOICES]
    return random.choice(lt_choices)


class ProductFactory(ModelFactory):
    name = factory.Faker('name')
    description = factory.Faker('text')
    license_type = factory.LazyFunction(get_license_type)
    line_type = factory.LazyFunction(get_line_type)

    class Meta:
        model = 'products.ProductBaseV2'
erichonkanen
  • 199
  • 1
  • 7
6

Because I had to do that for quite a lot of models, I came up with a more abstract version of erichonkanen's solution. I define a helper class, which I put in the top level test directory of my project and import it to the modules containing the factories:

test/helpers.py

import factory
import random


class ModelFieldLazyChoice(factory.LazyFunction):
    def __init__(self, model_class, field, *args, **kwargs):
        choices = [choice[0] for choice in model_class._meta.get_field(field).choices]
        super(ModelFieldLazyChoice, self).__init__(
            function=lambda: random.choice(choices),
            *args, **kwargs
        )

and in app/factories.py

from app.models import IceCreamProduct
from test.helpers import ModelFieldLazyChoice

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = ModelFieldLazyChoice(IceCreamProduct, 'type')
lmr2391
  • 571
  • 1
  • 7
  • 14
3

If you make the choices class-based...

class IceCreamProduct(models.Model):
    class ProductTypes(models.TextChoices):
        soft_ice_crem = (0, 'Soft Ice Cream')
        hard_ice_cream = (1, 'Hard Ice Cream')
        ...

class IceCreamProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = IceCreamProduct

    type = factory.fuzzy.FuzzyChoice(IceCreamProduct.ProductTypes)
    ...
xpeiro
  • 733
  • 5
  • 21
  • 1
    here's the documentation reference https://factoryboy.readthedocs.io/en/stable/recipes.html#fuzzying-django-model-field-choices – Marco Silva Jun 03 '22 at 15:38