Factory Boy random choice for a field with field option "choices"
Question:
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)
Answers:
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.
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'
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')
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
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)
...
Im using this:
from factory import LazyAttribute
from django.db.models import TextChoices
class CustomChoices(models.TextChoices):
ONE = 'one', 'First Option'
TWO = 'two', 'Second Option'
OTHER = 'other'
class CustomFactory(DjangoModelFactory):
system = LazyAttribute(lambda _: random.choice(CustomChoices.values))
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)
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.
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'
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')
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
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)
...
Im using this:
from factory import LazyAttribute
from django.db.models import TextChoices
class CustomChoices(models.TextChoices):
ONE = 'one', 'First Option'
TWO = 'two', 'Second Option'
OTHER = 'other'
class CustomFactory(DjangoModelFactory):
system = LazyAttribute(lambda _: random.choice(CustomChoices.values))