Skip to content

ModelFactory

A ModelFactory is a faker based model stub generator.

In the first step, building the factory class, you can define via FactoryFields 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:

  1. 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.
  2. 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:

  1. to_factory_field
  2. 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. When False, 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.