What's the best way to mock a Django User?

Viewed 340

I'm trying to instantiate a User instance through a Mock. That mock instance is being passed to another model Profile where I'm checking for any validation errors when a cleaning method is called.

However I'm getting: AttributeError: Mock object has no attribute '_state'

There is this previous post: How to mock users and requests in django. Nevertheless, I want to avoid any database calls.

What can be done differently so that a Mock will in this case?

#models.py

class Profile(models.Model):

    hobby = "Hobbyist"
    develop = "Developer"
    coding_level = (
        (hobby, hobby),
        (develop, develop)
    )

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    birth = models.DateField(verbose_name="Date Of Birth")
    coding_level = models.CharField(
        verbose_name="Experience",
        max_length=20,
        choices=coding_level, default=hobby, blank=False
    )
    bio = models.TextField(
        verbose_name="User Bio",
        validators=[MinValueValidator(10)]
    )
    github = models.URLField(
        verbose_name="GitHub link",
        validators=[check_submitted_link],
        unique=True
    )
    avatar = models.ImageField(upload_to="images/%Y/%m/%d/")
#test_models.py

class TestProfile__001(SimpleTestCase):

    def setUp(self):
        self.test_user = Mock(
            spec=User,
            username="test_username",
            email="test@email.com"
        )

        self.profile_data = {
            'user': self.test_user,
            'birth': '2019-10-07',
            'coding_level': 'hobbyist',
            'bio': "",
            'github': "http://www.test.com",
            'avatar': "image.txt"
        }

    def test_create_profile_fail(self):
        with self.assertRaises(ValidationError):
            test_profile = Profile(**self.profile_data)
            test_profile.clean_fields()
1 Answers

I would argue that there's no point in testing clean_fields, since it's already a part of very well tested Django codebase. But if you insist on testing it, you are definitely should not mock the User out.

Let's look at the code you are trying to test here (this is an excerpt from clean_fields):

        raw_value = getattr(self, f.attname)
        if f.blank and raw_value in f.empty_values:
            continue
        try:
            setattr(self, f.attname, f.clean(raw_value, self))
        except ValidationError as e:
            errors[f.name] = e.error_list

We see that it runs through every field on the model trying to call its clean method (source):

def clean(self, value):
    """
    Validate the given value and return its "cleaned" value as an
    appropriate Python object. Raise ValidationError for any errors.
    """
    value = self.to_python(value)
    self.validate(value)
    self.run_validators(value) 
    return value

OneToOneField itself does not introduce any of these methods, it's done higher in the class hierarchy, in ForeignKey class. And here's the main part of the validate method:

    using = router.db_for_read(self.remote_field.model, instance=model_instance)
    qs = self.remote_field.model._default_manager.using(using).filter(
        **{self.remote_field.field_name: value}
    )
    qs = qs.complex_filter(self.get_limit_choices_to())
    if not qs.exists():
        raise exceptions.ValidationError(
            self.error_messages['invalid'],
            code='invalid',
            params={
                'model': self.remote_field.model._meta.verbose_name, 'pk': value,
                'field': self.remote_field.field_name, 'value': value,
            },  # 'pk' is included for backwards compatibility
        )

As you can see, the whole validation consists of nothing but a query construction with a database call! So if you are trying to avoid hitting the database, you should just skip the validation of any foreign keys altogether:

def test_create_profile_fail(self):
    with self.assertRaises(ValidationError):
        test_profile = Profile(**self.profile_data)
        test_profile.clean_fields(exclude=['user'])  # don't forget to explain your decision in the comments

So to conclude, the best way to mock the User is to actually create it in the database. If you want to avoid the boilerplate, you can use the factory_boy package.

Related