raratiru
raratiru

Reputation: 9636

Patch a variable inside a method of a class instance

I am trying to understand how patching works and I am testing with pytest a Django View:

views.py

from django.contrib.auth.views import LoginView

class MyLoginView(LoginView):
    pass

test_view.py

from django.test import RequestFactory
from .views import MyLoginView

rf = RequestFactory()

def test_login(rf):
    request = rf.get(reverse('myapp:login'))
    response = MyLoginView.as_view()(request)
    assert response.status_code == 200

This fails because this View calls the database in order to get the current Site using the function get_current_site():

Failed: Database access not allowed

How can I mock get_current_site() to avoid the database hit?

An idea is to use a factory with pytest-factoryboy.

I managed to mock LoginView.get_context_data but I cannot go deeper:

from django.test import RequestFactory
from .views import MyLoginView

from django.contrib.sites.models import Site
from pytest_factoryboy import register
from unittest.mock import patch

rf = RequestFactory()


class SiteFactory(factory.Factory):
    class Meta:
        model = Site

register(SiteFactory)


def test_login_social(rf, site_factory):
    request = rf.get(reverse('myapp:login'))
    with patch(
        # 'django.contrib.auth.views.LoginView.get_context_data',  # This is wrong
        'django.contrib.auth.views.get_current_site',  # Solution: Patch where it is imported, this works!
        return_value=site_factory(name='example.com'),
    ):
        response = CommunityLoginView.as_view()(request)
    assert response.status_code == 200

Edit

The solution is to patch the method beeing called, at the scope where it is imported:

with patch('django.contrib.auth.views.get_current_site')


Here an error occurs due to the context_data being a <class 'django.contrib.sites.models.Site'>

How would you do it?

Upvotes: 0

Views: 1503

Answers (1)

escaped
escaped

Reputation: 744

You have two options here:

  1. pytest only allows a database access, if you explicitly mark the test function that we will hit the database. Without that information, pytest will run the test without having constructed a database for tests. I recommend to use pytest-django and the provided decorator pytest.mark.django_db.

  2. You have added the Site-Framework to your INSTALLED_APPS. This app is optional, but useful if you serve multiple different pages from a single Django application. There was a time when the Site-Framework was mandatory, but since it is optional I rarely include in my INSTALLED_APPS. Maybe you should leave it out to.


EDIT: Mocking

Sure, mocking should work as well, since every object in python is mockable (even small numbers). Keep in mind that you have to patch where the module/function is imported because it's bound to the local scope.

To find the right location, you could either search the Django source code, see how it is used and how to patch it correctly or try to drop into PDB. I am not sure which way will work, but I provide you with 2 options:

  1. pytest --pdb
  2. python -m pdb pytest. This will instantly open the debugger and you have to continue once. pytest will now run until the first exception occurs and PDB will start automatically.

You can now use bt (backtrace), u (walk stack up), l (show source code) and d (walk stack down) to find the location of the database access.


EDIT2: factoryboy

If you are using factoryboy, it depends on the build strategy whether it tries to access the database or not. The default strategy is .create(), which writes to the database.

It should work if you use site_factory.build(), since this will not access your database.

Upvotes: 1

Related Questions