Skip to main content

Generic factory for creating instances of Django models in tests.

Project description

django_factory is a generic implementation of Creation Methods for Django models.

Using django_factory in your tests means that each test can create instances of the models, but only has to specify the model attributes that are germane to that particular test.

Using creation methods usually leads to tests that are easier to read, and makes it easier to avoid convoluted test setup methods that are shared, as the pre-conditions for each test are easier to establish.

Lineage

django_factory is inspired by model_mommy but has several important improvements:

  • The creation methods are deterministic.

    • This ensures that your tests are repeatable, and you will have fewer eratic tests.

    • As the default values in the test are deterministic, if they suit a test they can be omitted. With random values you have to specify many more of the values, reducing the benefit you get from creation methods.

  • Model-specific creation logic can be specified on the model itself, encapsulating the logic there.

Use

The first thing to do is create an instance of the factory:

from django_factory import Factory

factory = Factory()

Often this will be done in the setUp method of a TestCase:

class ModelTests(TestCase):

    def setUp(self):
        super(ModelTests, self).setUp()
        self.factory = Factory()

If you are using testtools then you can pass your test case instance to the constructor in order to get better default values:

class ModelTests(DjangoTestCase, TesttoolsTestCase):

    def setUp(self):
        super(ModelTests, self).setUp()
        self.factory = Factory(test_case=self)

However, you don’t have to define this yourself, you can just subclass the provided TestCase or TransactionTestCase classes (corresponding to the Django classes of the same names) and start using self.factory in your tests:

from django_factory import TestCase

class ModelTests(TestCase):
    pass

Once you have a factory instance you can create instances of your models:

def test_model(self):
   instance = self.factory.make_one(Model)

This will create an instance of Model with arbitrary values for its attributes. If you need to fix particular attribute values then pass them in to the make_one method:

def test_model(self):
   instance = self.factory.make_one(Model, name="Mike")
   # Now instance.name == "Mike"

There are a few other methods that you might want to use:

def test_model(self):
   instances = self.factory.make(5, Model, name="Mike")
   # instances will now be a list of 5 Model instances, each
   # with name == 'Mike'

   instance = self.factory.prepare_one(Model, name="Mike")
   # This time the instance won't be saved, you'll have to
   # do that if you want.

   instances = self.factory.prepare(5, Model, name="Mike")
   # The same as make(), but without saving the instances.

Customising the behaviour

Frequently the generated values won’t be right for the particular model that you have. In these cases you can override the way that the values are created in order to suit your particular requirements.

This overriding is done by creating a “Factory” nested class on the model:

class Person(models.Model):

    name = models.CharField(max_length=100)
    active = models.BooleanField()

If we create an instance of Person using the factory, it will have active == False:

person = factory.make_one(Person)
print person.active

It may be that you want most of your tests to use active people, and just test with inactive people in a few cases. You could do this by passing the value every time:

person = factory.make_one(Person, active=True)

However this would be very repetitive. Instead you can override the default value using a nested class:

class Person(models.Model):

    name = models.CharField(max_length=100)
    active = models.BooleanField()

    class Factory:
        @staticmethod
        def get_active(field, factory):
            return True

Now if you don’t specify active when calling the factory, the person instance will have active == True:

person = factory.make_one(Person)
print person.active

The value for each field can be controlled in this way, by providing a method on the Factory nested class that is named get_ and the name of the field to control the value of.

The values passed to the get_active method are as follows:

  • field: the field instance the value is being generated for. This is useful if you have a function which is used for several fields, and want to include some information from the field in the value, such as the name:

    class Factory:
        @staticmethod
        def get_name(field, factory):
            return field.name
  • factory: the factory instance that is being used to generate the value. This is passed so that you can access the methods on the factory if needed. The methods that may be useful are:

    • prepare/prepare_one: if you need a model instance for a field then you can ask the factory to create one for you, passing in any important values.

    • getUniqueInteger: it is a good idea to include some uniqueness in the values that you are generating, so that you are less likely to get tests passing by accident, and it’s easier to track back from a value to where it was created. This is one of the methods the factory provides that will help with that. It will return an integer that will only be returned once by the factory in any particular test run.

    • getUniqueString/getUniqueUnicode/getUniqueDate/ getUniqueTime/getUniqueDateTime: similar to getUniqueInteger, but will return an object of type str, unicode, datetime.date, datetime.time, datetime.datetime respectively.

You can return any value from the method (including None) and it will be set on the resulting instance. However, the instance must then pass validation, so errors will be caught early.

By default, when the factory finds a ManyToManyField, it will create one instance of the referenced model for each instance of the model that it creates. You can control this behaviour by providing an attribute named number_of_ and the name of the ManyToManyField:

class Team(models.Model):

    people = models.ManyToManyField(Person)

    class Factory:
        number_of_people = 5

Lastly, if controlling individual fields is not sufficient, you can provide a single method, make_instance that is expected to create an instance of the model when called. This is necessary when there are complex inter-dependencies between the fields:

class Person(models.Model):

    name = models.CharField(max_length=100)
    active = models.BooleanField()

    class Factory:

        @staticmethod
        def make_instance(factory, name=None, active=None):
            if name is None:
                name = factory.getUniqueUnicode(prefix="name")
            if active is None:
                active = True
            return Person(name=name, active=active)

Again you are passed a factory instance in order to be able to get unique values to use in the model.

Note that it is expected that your make_instance method doesn’t save the model, otherwise prepare won’t work correctly with the model.

Custom field types

If you write a custom field you will want the factory to be able to generate values for that field.

In order to do that you need to write a function that returns a suitable value for the field when passed an instance of the field and an instance of the factory:

def generate_value_for_custom_field(field, factory):
    # Use uniqueness provided by factory, and inspect
    # field for any constraints that are relevant. Return
    # a suitable value

Once you have your function you can pass it to the factory constructor as part of the field_mapping:

field_mapping = Factory.DEFAULT_FIELD_MAPPING.copy()
field_mapping[CustomField] = generate_value_for_custom_field
factory = Factory(field_mapping=field_mapping)

Now you can use the factory to get an instance of any model using the custom field without having to override the generator at the model level.

Generators for models you don’t control

Sometimes you will want to customise the behaviour of models you don’t control. In those cases you can’t define a Factory nested class on the model, so instead you can control their behaviour through model_generators:

def create_user(factory, name=None, email=None):
    if name is None:
        name = factory.getUniqueUnicode(prefix="name")
    if email is None:
        email = factory.getUniqueString() + "@example.com"
    return User(name=name, email=email)

model_generators = {User: create_user}
factory = Factory(model_generators=model_generators)

Now when you call

factory.make_one(User)

it will call your create_user method to create the instance.

Known Limitations

  • Using ManyToManyField with through=<model> breaks down when <model> has multiple ForeignKeys pointing to one of the models invovled in the ManyToManyField relationship. Currently you have to use number_of_<field> = 0 to avoid any instances being created, and then create them yourself.

  • ContentType``s (and the associated classes, e.g. ``GenericRelation) will not be created with valid values without using model_generators.

  • FilePathField is not currently supported in the default field_mapping.

  • FileField and ImageField do not create files on the filesystem, and you cannot use the save etc. methods on the results. You can override this either by changing field_mapping, or using a Factory nested class on your models to create the specific fields as needed.

Changelog

0.3

  • Add model_generators to allow functions to be provided to create instances of models where you can’t add a nested Factory class, or don’t want to use the default.

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_factory-0.4.tar.gz (28.8 kB view details)

Uploaded Source

Built Distribution

django_factory-0.4-py2.7.egg (23.0 kB view details)

Uploaded Source

File details

Details for the file django_factory-0.4.tar.gz.

File metadata

File hashes

Hashes for django_factory-0.4.tar.gz
Algorithm Hash digest
SHA256 c77ac143f86880d6ae079205352af00a0705fec18d7941d17117e2af1de36cb8
MD5 885c01755a69352d2f01b932302a11fe
BLAKE2b-256 11239c7e1246d26aa6ec83003112d21737220431de05ccbfdaaf7255a162c220

See more details on using hashes here.

File details

Details for the file django_factory-0.4-py2.7.egg.

File metadata

File hashes

Hashes for django_factory-0.4-py2.7.egg
Algorithm Hash digest
SHA256 4705c51e5a2a630924b875e5fee3b7ec9feac34e2722a51fb99b806aceadc648
MD5 d9fec06d67cac390fa9351edb0fd60c3
BLAKE2b-256 cfc45da6c43b31c82c488322e7c5baa957a0c53abc1ff467fdb774f435108588

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