berinhard/model_mommy

Implement lazy_related for use in override attrs in each TestCase

danizen opened this issue · 1 comments

Current behavior

I'm attempting to address the shortcoming of creating multiple models related to a single instance of a recipe, as best described in #144.

This is in the context of a single django.test.TestCase, or when implemented as a fixture for pytest-django.

Theory of design

The last work on this identified the essential problem of a mommy remembering what it had created. I think it is better if something like related remembers what it has created, but since an mommy_recipes file is imported once per test run, we'd best do that in the unit test setUp or in a pytest-django fixture.

To do that well, we need some syntactic sugar that makes it easy to use delegate creation to an object that will return the related models once. Here, I take advantage of the tight coupling between model_mommy and Django and use django.utils.functional.SimpleLazyObject to substitute the results returned by model_mommy.recipe.related.

Suggested behavior

In myapp/mommy_recipes.py:

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from faker import Faker
from model_mommy.recipe import Recipe, seq

faker = Faker()

User = get_user_model()

basicuser = Recipe(User,
    username=seq('rich'),
    email=faker.email,
    first_name=faker.first_name,
    last_name=faker.last_name,
    is_staff=False,
    is_superuser=False,
)

staffuser = basicuser.extend(
    username=seq('bob'),
    is_staff=True,
)

admingroup = Recipe(Group, name='Admin')

In myapp/tests.py:

from model_mommy import mommy
from model_mommy.recipe import lazy_related
from django.test import TestCase

class TestLazyRelated(TestCase):

     def setUp(self):
          self.admins = mommy.make_recipe('myapp.staffuser', 
              groups=lazy_related('admingroup'),
              _quantity=2,
          )

    def test_group_is_unique(self):
        user_count = User.objects.count()
        assert user_count == 3

        group_count = Group.objects.count()
        assert group_count == 1

        admingroup = Group.objects.filter(name='Admin').first()

        assert all(user.groups.count() == 1 for user in User.objects.all())
        assert all(group.id == admingroup.id for user in User.objects.all() for group in user.groups.all())

Suggested Implementation

Added to model_mommy\recipes.py:

class __callable_related(related):
    """Make related callable so we can use SimpleLazyObject"""
    # don't export this class; it could interfere with related being called as a recipe
    def __init__(self, *args):
        super().__init__(*args)

    def __call__(self):
        return self.make()

# should be at top
from django.utils.functional import SimpleLazyObject

def lazy_related(*args):
    """
    If used in mommy_recipes, this solution will backfire, because the database will be rolled back.
    Only use lazy_related in class-based test cases or when creating pytest-django fixtures.

    {{ additional_documentation }}
    """
    return SimpleLazyObject(__callable_related(*args))

Example of pytest-django fixture:

import pytest
from model_mommy import mommy
from model_mommy.recipe import lazy_related


@pytest.mark.django_db
@pytest.fixture
def users_with_group():
    return mommy.make_recipe('myapp.staffuser', 
          groups=lazy_related('admingroup'),
          _quantity=2,
    )

Reproduction Steps

Lots of instances of the problem in #144 and others.

Versions

For me, I'm working with the following versions at the moment:

Python: CPython 3.6.8
Django: 2.2.5
Model Mommy: 1.6.0

Ok - my code was working because of the Group to User is a many-to-many field, and I need to repeat with multple calls to make_recipe to double check.