0

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?

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you! Actually, I do not intend to test Django. This is just a tricky example to understand 'how' and how far patch can make it.
@raratiru I updated my answer to help in finding the correct way to mock the given function. Maybe there are also multiple accesses to the database beside get_current_site().
Thank you for the pdb trick and the links. I managed to mock LoginView.get_context_data but I cannot go deeper and find either current_site or get_current_site
ahh... now I see the problem! I somehow missed that you try to create a Site object using factoryboy. If you create an instance with 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 does not access your database.
Thank you, it does not hit the database, because I managed to patch the get_context_data method without hitting the database. Actually, I cannot patch the variable defined inside that method. class() -> def method(self) -> variable = call_to_another_module().
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.