ModelFactory¶
A ModelFactory is a faker based model stub generator.
In the first step, building the factory class, you can define via FactoryField
s customizations of the parameters passed
for the fakers for the model.
The second step, is making a factory instance. Here can values be passed which should be used for the model. They are baked in the factory instance. But you are able to overwrite them in the last step or to exclude them.
The last step, is building a stub model via build
. This is an instance-only method not like the other build method other model definitions.
In short the sequence is:
Factory definition -> Factory instance -> Factory build method -> stubbed Model instance to play with.
You can reuse the factory instance to produce a lot of models.
Example:
import enum
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
user_factory = UserFactory(language="eng")
user_model_instance = user_factory.build()
# provide the name edgy
user_model_instance_with_name_edgy = user_factory.build(overwrites={"name": "edgy"})
Now we have a basic working model. Now let's get more complicated. Let's remove the implicit id field via factory fields
import enum
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
# disable the implicit id field
disable_id = FactoryField(exclude=True, name="id")
user_factory = UserFactory()
user_model = user_factory.build()
Note
Every Factory class has an own internal faker instance. If you require a separate faker you have to provide it in the build method
as faker
keyword parameter.
Parametrize¶
For customization you have two options: provide parameters to the corresponding faker method or to provide an own callable which can also receive parameters. When no callback is provided the mappings are used which use the field type name of the corresponding edgy field.
E.g. CharFields use the "CharField" mapping.
from typing import Any
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
from faker import Faker
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
def name_callback(field_instance: FactoryField, faker: Faker, parameters: dict[str, Any]) -> Any:
return f"{parameters['first_name']} {parameters['last_name']}"
class UserFactory(ModelFactory):
class Meta:
model = User
# strings are an abbrevation for faker methods
language = FactoryField(
callback=lambda field_instance, faker, parameters: faker.language_code(**parameters)
)
name = FactoryField(
callback=name_callback,
parameters={
"first_name": lambda field_instance, faker, parameters: faker.first_name(),
"last_name": lambda field_instance, faker, parameters: faker.last_name(),
},
)
user_factory = UserFactory()
# now the name is composed by two names
user_model = user_factory.build()
# now the name is composed by two names and both names are edgy
user_model = user_factory.build(parameters={"name": {"first_name": "edgy", "last_name": "edgy"}})
You can also overwrite the field_type on FactoryField base. This can be used to parametrize fields differently. E.g. ImageFields like a FileField or a CharField like PasswordField.
import enum
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
password = edgy.fields.CharField(max_length=100)
icon = edgy.fields.ImageField()
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
password = FactoryField(field_type="PasswordField")
icon = FactoryField(field_type=edgy.fields.FileField)
user_factory = UserFactory()
# now the password uses the password field default mappings and for ImageField the FileField defaults
user_model = user_factory.build()
In case you want to overwrite a mapping completely for all subclasses you can use the Meta mappings
attribute.
import enum
import edgy
from edgy.testing.factory.mappings import DEFAULT_MAPPING
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
password = edgy.fields.PasswordField(max_length=100)
icon = edgy.fields.ImageField()
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
mappings = {"ImageField": DEFAULT_MAPPING["FileField"], "PasswordField": None}
class UserSubFactory(UserFactory):
class Meta:
model = User
user_factory = UserFactory()
# now the password is excluded and for ImageField the FileField defaults are used
user_model = user_factory.build()
# this is inherited to children
user_model = UserSubFactory().build()
Setting a mapping to None
will disable a stubbing by default.
You will need to re-enable via setting the mapping in a subclass to a mapping function.
import enum
import edgy
from edgy.testing.factory.mappings import DEFAULT_MAPPING
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
# the true user password simulator
def PasswordField_callback(field: FactoryField, faker: Faker, parameters: dict[str, Any]) -> Any:
return faker.random_element(["company", "password123", "querty", "asdfg"])
class User(edgy.Model):
password = edgy.fields.PasswordField(max_length=100)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
mappings = {"PasswordField": PasswordField_callback}
user_factory = UserFactory()
# now PasswordFields use a special custom mapping which provides common user passwords
user_model = user_factory.build()
Tip
You can name a FactoryField differently and provide the name parameter explicitly. This way it is possible to workaround occluded fields.
Setting database and schema¶
By default the database and schema of the model used is unchanged. You can however provide an other database or schema than the default by defining them as class or instance variables (not by keyword arguments) on a Factory. The syntax is the same as the one used for database models, you define them on the main model. You can also overwrite them one-time in the build method.
__using_schema__
(str or None)database
(Database or None)
Note
There is a subtle difference between database models and ModelFactories concerning __using_schema__
.
When None
in ModelFactory
the default of the model is used while in database models None selects the main schema.
Parametrizing relation fields¶
Relation fields are fields like ForeignKey ManyToMany, OneToOne and RelatedField.
To parametrize relation fields there are two variants:
- Pass
build()
parameters as field parameters. For 1-n relations there are two extra parameters min=0, max=100, which allow to specify how many instances are generated. - Transform a ModelFactory to a FactoryField.
The first way cannot be used with RelatedFields, which are automatically excluded. You can however pass values to them via the second way.
To transform a ModelFactory there are two helper classmethods:
to_factory_field
to_list_factory_field(min=0, max=100)
Special parameters¶
There are two special parameters which are always available for all fields:
- randomly_unset
- randomly_nullify
The first randomly excludes a field value. The second randomly sets a value to None. You can either pass True for a equal distribution or a number from 0-100 to bias it.
Excluding a field¶
To exclude a field there are three ways
- Provide a field with
exclude=True
. It should be defined under the name of the value. - Add the field name to the exclude parameter of build.
- Raise
edgy.testing.exceptions.ExcludeValue
in a callback.
Let's revisit one of the first examples. Here the id field is excluded by a different named FactoryField.
import enum
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
# disable the implicit id field
disable_id = FactoryField(exclude=True, name="id")
user_factory = UserFactory()
user_model = user_factory.build()
Note: However that the FactoryField can only be overwritten by its provided name or in case it is unset its implicit name. When multiple fields have the same name, the last found in the same class is overwritting the other.
Otherwise the mro order is used.
Here an example using both other ways:
import enum
import edgy
from edgy.testing.exceptions import ExcludeValue
from edgy.testing.factory import ModelFactory, FactoryField
test_database = DatabaseTestClient(...)
models = edgy.Registry(database=...)
def callback(field_instance, faker, parameters):
raise ExcludeValue
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback=callback)
user_factory = UserFactory()
user_model_instance = user_factory.build(exclude={"name"})
Build¶
The central method for factories is build()
. It generates the model instance.
It has also some keyword parameters for post-customization. They are also available for default relationship fields
or for wrapping factory fields via the to_factory_field
or to_list_factory_field
classmethods.
The parameters are:
- faker (not available for factories for relationship fields. Here is the provided faker or faker of the parent model used). Provide a custom Faker instance. This can be useful when the seed is modified.
- parameters ({fieldname: {parametername: parametervalue} | FactoryCallback}): Provide per field name either a callback which returns the value or parameters.
- overwrites ({fieldname: value}): Provide the value directly. Skip any evaluation
- exclude (e.g. {"id"}): Exclude the values from stubbing. Useful for removing the autogenerated id.
- database (Database | None | False): Use a different database. When None pick the one of the ModelFactory if available, then fallback to the model.
When
False
, just use the one of the model. - schema (str | None | False): Use a different schema. When
None
pick the one of the ModelFactory if available, then fallback to the model. WhenFalse
, just use the one of the model.
import enum
import edgy
from edgy.testing.client import DatabaseTestClient
from edgy.testing.factory import ModelFactory, FactoryField
test_database1 = DatabaseTestClient(...)
test_database2 = DatabaseTestClient(...)
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
password = edgy.fields.PasswordField(max_length=100)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
database = test_database1
__using_schema__ = "test_schema1"
user_factory = UserFactory(language="eng")
user_model_instance = user_factory.build()
# customize later
user_model_instance_with_name_edgy = user_factory.build(
overwrites={"name": "edgy"},
parameters={"password": {"special_chars": False}},
exclude={"language"},
)
# customize later, with different database and schema
user_model_instance_with_name_edgy = user_factory.build(
overwrites={"name": "edgy"},
parameters={"password": {"special_chars": False}},
exclude={"language"},
database=test_database2,
schema="test_schema2",
)
Saving¶
Saving isn't done in build(). It must be done after seperately. When requiring saving you should exclude autoincrement fields otherwise strange collissions can happen.
import enum
import edgy
from edgy.testing.factory import ModelFactory, FactoryField
models = edgy.Registry(database=...)
class User(edgy.Model):
name: str = edgy.CharField(max_length=100, null=True)
language: str = edgy.CharField(max_length=200, null=True)
class Meta:
registry = models
class UserFactory(ModelFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
# disable the implicit id field
disable_id = FactoryField(exclude=True, name="id")
# you can also build an autosave factory which saves after building
class UserAutoSaveFactory(UserFactory):
class Meta:
model = User
language = FactoryField(callback="language_code")
@classmethod
def build(cls, **kwargs):
return edgy.run_sync(super().build(**kwargs).save)
user_factory = UserFactory(language="eng")
user_model_instance = user_factory.build()
edgy.run_sync(user_model_instance.save())
# or the UserAutoSaveFactory
UserAutoSaveFactory(language="en").build()
Model Validation¶
By default a validation is executed if the model can ever succeed in generation. If not an error is printed but the model still builds. If you dislike this behaviour, you can disable the implicit model validation via:
class UserFactory(ModelFactory, model_validation="none"):
...
You have following options:
none
: No implicit validation.warn
: Warn for unsound factory/model definitions which produce other errors than pydantic validation errors. Default.error
: Same as warn but reraise the exception instead of a warning.pedantic
: Raise even for pydantic validation errors.