How to update/change attribute in Django Class Based View Test

Viewed 21

I created a mixin that requires an attribute to be added to a Class Based View (cbv) and it works as expected - but I'm having a tough time 'testing' that mixin specifically.

Here is my test:

class TestView(GroupRequiredMixin, View):
    group_required = ("test_group",)
    raise_exception = True

    def get(self, request):
        return HttpResponse("OK")

class GroupRequiredMixinTest(TestCase):

    def setUp(self):
        group = GroupFactory(name="test_group")
        self.user = UserFactory(groups=(group,))

    def test_view_without_group_required_improperly_configured(self):
        rfactory = RequestFactory()
        request = rfactory.get("/fake-path")
        request.user = self.user
        view = TestView()
        view.group_required = None
        with self.assertRaises(ImproperlyConfigured):
            view.as_view()(request)

This test errors with this message: AttributeError: This method is available only on the class, not on instances.

What is the 'appropriate' way to test this?

Should I be setting up a unique class for each situation I want to test? Or is there a way I can dynamically change that attribute on an instance like in my test that fails?

*Edit to add the mixin (almost identical to the core Django version for Permissions):

class GroupRequiredMixin(AccessMixin):
    """Verify that the current user has all specified groups."""

    group_required = None

    def handle_no_group(self):
        if self.raise_exception or self.request.user.is_authenticated:
            raise PermissionDenied(self.get_permission_denied_message())

        path = self.request.build_absolute_uri()
        resolved_login_url = resolve_url(self.get_login_url())
        # If the login url is the same scheme and net location then use the
        # path as the "next" url.
        login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
        current_scheme, current_netloc = urlparse(path)[:2]
        if (not login_scheme or login_scheme == current_scheme) and (
            not login_netloc or login_netloc == current_netloc
        ):
            path = self.request.get_full_path()
        return redirect_to_login(
            path,
            resolved_login_url,
            self.get_redirect_field_name(),
        )

    def get_group_required(self):
        """
        Override this method to override the group_required attribute.
        Must return an iterable.
        """
        if self.group_required is None:
            raise ImproperlyConfigured(
                f"{self.__class__.__name__} is missing the "
                f"group_required attribute. Define "
                f"{self.__class__.__name__}.group_required, or override "
                f"{self.__class__.__name__}.get_group_required()."
            )
        if isinstance(self.group_required, str):
            groups = (self.group_required,)
        else:
            groups = self.group_required
        return groups

    def has_group(self):
        """
        Override this method to customize the way groups are checked.
        """
        groups = self.get_group_required()
        if hasattr(self.request.user, "has_groups"):
            return self.request.user.has_groups(groups)
        else:
            return self.handle_no_group()

    def dispatch(self, request, *args, **kwargs):
        if not self.has_group():
            return self.handle_no_group()
        return super().dispatch(request, *args, **kwargs)
        with self.assertRaises(ImproperlyConfigured):
>           view.as_view()(request)

my_app/tests/test_mixins.py:52: 
_ _ _ _ _ 
    def __get__(self, instance, cls=None):
        if instance is not None:
>           raise AttributeError("This method is available only on the class, not on instances.")
E           AttributeError: This method is available only on the class, not on instances.
1 Answers

I was able to get it working with the following:

        rfactory = RequestFactory()
        request = rfactory.get("/fake-path")
        request.user = self.user
        view = TestView.as_view(group_required=None)
        with self.assertRaises(ImproperlyConfigured):
            view(request)

I could not call TestView() to instantiate a new view - it needed to be TestView.as_view() to instantiate it. Then the arguments get passed inside the as_view() portion.

Working as intended now.

Related