Skip to main content

A small python / django model designed to decouple creation of models for testing from the creation of models in production to make updating tests less painful.

Project description

https://travis-ci.com/publons/django-test-model-builder.svg?token=WSHb2ssbuqzAyoqCvdCs&branch=master

A small python / django model designed to decouple creation of models for testing from the creation of models in production to make updating tests less painful.

Quickstart

Create generic models without defining fields

class User(AbstractBaseUser):
    username = models.CharField()

class UserBuilder(ModelBuilder):
    model = User

    def get_default_fields(self):
        return {'username': 'test_username'}

user = UserBuilder().build()
print(user.username)
>>> test_username

Override defaults when required

user = UserBuilder().with_username('test').build()
>>> user.username
>>> test

Create multiple models with the same values

builder = UserBuilder().with_username('test')
user_1 = builder.build()
user_2 = builder.build()

user_1.username == user_2.username
>>> True

user_1 == user_2
>>> False

Update models without updating tests

class User(AbstractBaseUser):
    username = models.CharField()
    dob = models.DateField()

class UserBuilder(ModelBuilder):
    model = User
    def get_default_fields(self):
        return {
            'username': random_string,
            'dob': date(1990, 1, 1),
        }

user = UserBuilder().build()

user.dob
>>> date(1990, 1, 1)

user = (
    UserBuilder()
    .with_dob(date(2000, 1, 1))
    .build()
)

user.dob
>>> date(2000, 1, 1)

Setting defaults

The get_default_fields returns a dictionary used to populate any unset model fields when the model is created. These can be values or callables if you need to delay the creation of models until it is needed or want to generate random data for each instance to avoid breaking database constraints.

class UserBuilder(ModelBuilder):
    model = User

    def get_default_fields():
        return {
            # Callable, each user will have a random username.
            'username': random_string,

            # Value, each user will have the same date of birth.
            'dob': date(1990, 1, 1),

            # Called with uninitiated build() call so duplicate model isn't
            # generated until comparison with any custom `with_` setter
            # functions, this field will be thrown away
            # if custom setter is present. You can also use a
            # lambda to achieve the same thing.
            'user': UserBuilder().build
    }

Providing custom values using the “with_” prefix

with_ functions are dynamically generated, these are used to override defaults.

class UserBuilder(ModelBuilder):
    model = User
    def get_default_fields():
        return {
            'username': random_string,
            'dob': date(1990, 1, 1),
        }

user = UserBuilder().with_dob(date(2019, 10, 10)).build()
user.dob
>>> date(2019, 10, 10)

All these functions do is set the passed in value as the function name in an internal dictionary. This pattern can be used to create more readable tests.

Any function prefixed with with_ is automatically wrapped with a function that returns a copy of the builder for side-effect-free chaining.

You can also explicitly define these with_<> on the ModelBuilder subclass to add your own implementation.

from datetime import timedelta

class UserBuilder(ModelBuilder):
    model = User
    def get_default_fields():
        return {
            'username': random_string,
            'dob': date(1990, 1, 1)
        }

    def with_under_18():
        self.data['dob'] = date.today() - timedelta(years=17)

UserBuilder().under_18().build()

Finally the with_ prefix is adjustable in case you have a blocking field that you want use. For example you can change this to use the prefix set_ by going

class CustomAuthorBuilder(AuthorBuilder):
    dynamic_field_setter_prefix = 'set_'

author = (
    CustomAuthorBuilder()
    .set_publishing_name('Billy Fakeington')
    .build()
)

author.publishing_name
>>> 'Billy Fakeington'

Calling .build()

Building the model is broken into four steps.

  • Prepare the data dictionary.

  • Perform pre processing.

  • Create the instance.

  • Perform post possessing.

There is also a save_to_db kwarg that can be set to optionally persist the built model to memory only for use in more complicated tests.

Perform pre processing

By default this method changes models to their their _id suffix. This can be extended to perform additional preprocessing of fields.

from datetime import timedelta

class UserBuilder(ModelBuilder):
    model = User
    def get_default_fields():
        return {
            'username': random_string,
            'dob': date(1990, 1, 1),
        }

    def pre(self):
        self['dob'] += timedelta(days=1)

UserBuilder().build().dob
# date(1990, 1, 2)

If you wanting to add non field values for accession by the pre/post hooks you can override the get_builder_context call to load any extra fields which will be made available to the self.data dict after the initial model fields have been set, for instance:

class AuthorBuilder(ModelBuilder):

    def get_default_fields():
        return {
            'username': random_string,
            'dob': date(1990, 1, 1)
        }

    def get_builder_context(self):
        return {
            'email_address': fake_email
        }

    def post(self):
        print(self.dict)

AuthorBuilder().build()
>>> {
>>>     'username': random_string,
>>>     'dob': date(1990, 1, 1),
>>>     'email_address': fake_email
>>> }

Create the instance

By default instances are created by calling model.objects.create with the models fields from the data dictionary. This behavior can be changed by overriding the builders .create method, this method must set the builders instance attribute`self.instance = …`.

class UserBuilder(ModelBuilder):
    model = User

    def get_default_fields():
        return {
            'username': random_string,
        }

    def create(self):
        model = self.get_model()
        try:
            instance = self.model.objects.get(
                username=self.data['username']
            )
        except model.objects.DoesNotExist:
            super(UserBuilder, self).create()

builder = UserBuilder().with_username('test')
user_1 = builder.build()
user_2 = builder.build()

user_1 == user_2
>>> True

Preform post processing

Post processing is carried out after the instance has been created. By default it does nothing, but provides a useful place to do things like add related models.

class UserBuilder(ModelBuilder):
    model = User

    def get_default_fields():
        return {
            'username': random_string,
        }

    def with_emails(*args):
        self.data['emails'] = args

    def post(self):
        for email in self.data.get('emails', []):
            (
                EmailBuilder()
                .with_address(email)
                .with_user(self.instance)
                .build()
            )

user = (
    UserBuilder()
    .with_emails(random_email(), random_email())
    .build()
)

user.email_set.count()
>>> 2

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-test-model-builder-0.0.2.tar.gz (5.8 kB view details)

Uploaded Source

Built Distribution

File details

Details for the file django-test-model-builder-0.0.2.tar.gz.

File metadata

  • Download URL: django-test-model-builder-0.0.2.tar.gz
  • Upload date:
  • Size: 5.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/2.0.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/42.0.1 requests-toolbelt/0.9.1 tqdm/4.39.0 CPython/3.6.9

File hashes

Hashes for django-test-model-builder-0.0.2.tar.gz
Algorithm Hash digest
SHA256 1ed3b00a09bb7a92d0d357d8a118eba9924ac900d7e21ac0fde82cd6237521fb
MD5 216b2efeea95dd35878b18e640e4546e
BLAKE2b-256 2bcbb4240e41628314fdbfa938a9f9102a9639f776329890452fb7ba56341e20

See more details on using hashes here.

File details

Details for the file django_test_model_builder-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: django_test_model_builder-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 6.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/2.0.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/42.0.1 requests-toolbelt/0.9.1 tqdm/4.39.0 CPython/3.6.9

File hashes

Hashes for django_test_model_builder-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b515eb8d3c3a328ca038b155f1b8e5616ca99dd48c7cf487c271eaf486a51b46
MD5 25163fee888b2044176fad5c34924089
BLAKE2b-256 f459e887dc56a75f17241456746931ef7c32d519f8f4a49b852246da0bfaa306

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page