Skip to main content

A model instance factory for pytest-django

Project description

pytest-django-ifactory

A model instance factory for pytest-django.

Motivation

pytest-django-ifactory makes it easy to create model instances for your test cases even if they contain a lot of non-nullable fields and complex foreign key relationships. If you every felt like you spent too much time coming up with dummy values for you models' fields just to get an instance into the database to use in a test then pytest-django-ifactory might be for you.

pytest's fixtures are great and perfect if you want to have a static model instance available in the database for your tests. Problems arise however when you want to vary one of its fields. To reuse your fixture in that case you need to modify the field and save the instance back to the database (assuming your test needs it in the database, of course). This results in at least two lines of extra setup code in you test case and an extra call to the database.

pytest-django-ifactory is simply an instance factory (hence ifactory) function that automatically detects your Django models and tries to come up with acceptable defaults for the fields you don't care about. This includes generating unique values for fields marked as unique and to create related instances for non-nullable foreign keys.

Usage

This plugin comes with two fixtures: ifactory and transactional_ifactory. Use them when you need to put model instances in the database. ifactory and transactional_ifactory are identical except that the latter uses pytest-django's transactional_db fixture instead of the db fixture. See pytest-django's and Django's documentation for when you would want to use it. Below is a contrived example to test a function that finds duplicate names of your users:

from itertools import groupby
from operator import methodcaller

from django.contrib.auth.models import User


def get_duplicate_names():
    all_users = User.objects.order_by("last_name", "first_name")
    users_by_name = groupby(all_users, methodcaller("get_full_name"))
    return [full_name for full_name, users in users_by_name if len(list(users)) > 1]


def test_get_duplicate_names(ifactory):
    ifactory.auth.user(first_name="Albert", last_name="Einstein")
    ifactory.auth.user(first_name="Albert", last_name="Einstein")
    ifactory.auth.user(first_name="Erwin", last_name="Schrodinger")
    ifactory.auth.user(first_name="Richard", last_name="Feynman")
    assert get_duplicate_names() == ["Albert Einstein"]
    assert User.objects.count() == 4

You find you models namespaced by the application name and the model name on the ifactory instance. For instance, if you have created a Book model in a library application (and put your library application in INSTALLED_APPS), its factory function will be ifactory.library.book(). This function accepts the same arguments as your model constructor does and returns the created instance.

Notice in the example above that we create four new users without specifying their unique usernames. The goal of this project is that you should never have to specify the value of a field if you're not interested in that value in your test. Conversely, you should never rely on a value you haven't explicity set. This library gives no guarantees that the value it chooses for a field will be the same the next time the test is run. It just tries to make sure that the instance can be saved to the database without any integrity errors.

If you're running a type checker on your code, prefer the create() method on ifactory to get the correct return type:

ifactory.create(User, first_name="Albert", last_name="Einstein")

While I would recommend against it, if you do want to know which default value will be used, you can use the pytest_django_ifactory_configure hook. A good place to put it is in your conftest.py:

def pytest_django_ifactory_configure(ifactory):
    ifactory.configure_defaults(
        "auth.user", first_name="Albert", last_name="Einstein"
    )

From now on, all users in your tests will be Albert Einstein unless you say otherwise:

def test_albert_by_default(ifactory):
    albert = ifactory.auth.user()
    assert albert.get_full_name() == "Albert Einstein"
    not_albert = ifactory.auth.user(first_name="Erwin", last_name="Schrodinger")
    assert not_albert.full_name() == "Erwin Schrodinger"

While the above example might not be the best idea as your test suit grows the hook can be used to enforce constraints in your models that this library is unaware of, e.g., validation errors raised by your models that depend on the model's field values.

Development

This project uses ruff to lint and format the code, mypy to type check it, pytest to test it, and finally check-manifest to check the project's MANIFEST.in. To facilitate (and remember) to actually run all these tools, pre-commit is used. Hence, the only hard development requirement is pre-commit. Install it and run

$ pre-commit install

once the first time you check out the repo and from now on all checks except the type and unit tests will be run everytime a commit is made. The type checking is run manually with mypy,

$ mypy .

and the unit tests are run with pytest

$ pytest

Gitlab CI is configured to run the tests with Python 3.9-3.13 and all supported Django LTS versions.

If you want to install all development requirements to run them manually (and without using pre-commit run --all-files), use the optional dev requirement group:

$ python -m pip install -e .[dev]

License

Like pytest-django, pytest-django-ifactory is released under the BSD 3-clause license.

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

pytest_django_ifactory-1.3.0.tar.gz (20.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_django_ifactory-1.3.0-py3-none-any.whl (11.7 kB view details)

Uploaded Python 3

File details

Details for the file pytest_django_ifactory-1.3.0.tar.gz.

File metadata

  • Download URL: pytest_django_ifactory-1.3.0.tar.gz
  • Upload date:
  • Size: 20.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.9.22

File hashes

Hashes for pytest_django_ifactory-1.3.0.tar.gz
Algorithm Hash digest
SHA256 07886243b01339db8ef2c1769c4678a88053aec523dbd4647f4fa12ad94a414b
MD5 9446bf018f9a65f749684a492c85ca98
BLAKE2b-256 bdf450701bd5ac3a430c2da0c5fc2fe50b4fbd248f6fce70f96c6cdc272bff6f

See more details on using hashes here.

File details

Details for the file pytest_django_ifactory-1.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_django_ifactory-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6746e45613fbcd3744e947ed5d231a94376d5fad9701f6fca2c6156fd14c548f
MD5 6a9167f0513384ba7a245885270a543c
BLAKE2b-256 e351816700baee28dbb937f86e7ffe282efba163b25b5a6c7b2c5284f7a3efb4

See more details on using hashes here.

Supported by

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