Django Rest Framework - How to test ViewSet?
Asked Answered
B

6

37

I'm having trouble testing a ViewSet:

class ViewSetTest(TestCase):
    def test_view_set(self):
        factory = APIRequestFactory()
        view = CatViewSet.as_view()
        cat = Cat(name="bob")
        cat.save()

        request = factory.get(reverse('cat-detail', args=(cat.pk,)))
        response = view(request)

I'm trying to replicate the syntax here:

http://www.django-rest-framework.org/api-guide/testing#forcing-authentication

But I think their AccountDetail view is different from my ViewSet, so I'm getting this error from the last line:

AttributeError: 'NoneType' object has no attributes 'items'

Is there a correct syntax here or am I mixing up concepts? My APIClient tests work, but I'm using the factory here because I would eventually like to add "request.user = some_user". Thanks in advance!

Oh and the client test works fine:

def test_client_view(self):
    response = APIClient().get(reverse('cat-detail', args=(cat.pk,)))
    self.assertEqual(response.status_code, 200)
Brussels answered 15/4, 2014 at 0:38 Comment(1)
I was trying to get force auth to work, as per their docs but it doesn't seem to work. I don't use drf native auth tokens but my own jwt tokens so can't get it going via APIRequestFactory.Bedford
B
33

I think I found the correct syntax, but not sure if it is conventional (still new to Django):

def test_view_set(self):
    request = APIRequestFactory().get("")
    cat_detail = CatViewSet.as_view({'get': 'retrieve'})
    cat = Cat.objects.create(name="bob")
    response = cat_detail(request, pk=cat.pk)
    self.assertEqual(response.status_code, 200)

So now this passes and I can assign request.user, which allows me to customize the retrieve method under CatViewSet to consider the user.

Brussels answered 15/4, 2014 at 18:34 Comment(0)
U
19

I had the same issue, and was able to find a solution.

Looking at the source code, it looks like the view expects there to be an argument 'actions' that has a method items ( so, a dict ).

https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/viewsets.py#L69

This is where the error you're getting is coming from. You'll have to specify the argument actions with a dict containing the allowed actions for that viewset, and then you'll be able to test the viewset properly.

The general mapping goes:

{
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
}

http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers

In your case you'll want {'get': 'retrieve'} Like so:

class ViewSetTest(TestCase):
    def test_view_set(self):
        factory = APIRequestFactory()
        view = CatViewSet.as_view(actions={'get': 'retrieve'}) # <-- Changed line
        cat = Cat(name="bob")
        cat.save()

        request = factory.get(reverse('cat-detail', args=(cat.pk,)))
        response = view(request)

EDIT: You'll actually need to specify the required actions. Changed code and comments to reflect this.

Uncomfortable answered 5/9, 2014 at 21:26 Comment(0)
R
10

I found a way to do this without needing to manually create the right viewset and give it an action mapping:

from django.core.urlresolvers import reverse, resolve
...
url = reverse('cat-list')
req = factory.get(url)
view = resolve(url).func
response = view(req)
response.render()
Range answered 1/10, 2015 at 19:52 Comment(0)
E
4

I think it's your last line. You need to call the CatViewSet as_view(). I would go with:

response = view(request)

given that you already defined view = CatViewSet.as_view()

EDIT:

Can you show your views.py? Specifically, what kind of ViewSet did you use? I'm digging through the DRF code and it looks like you may not have any actions mapped to your ViewSet, which is triggering the error.

Ersatz answered 15/4, 2014 at 0:45 Comment(4)
last line, I'm able to go to the debugger before it, everything is defined but I think maybe there's something special about passing request into ViewSet?Brussels
Try passing the pk in last line. Check out the Rendering Responses example on that same page. Third line: response = view(request, pk='4')Ersatz
hmm I've tried passing it in, same error for view(request, pk=cat.pk) and view(request, cat.pk)Brussels
Can you post your urls.py and views.py file? Also, you could try changing your second-to-last line to use a hard-coded path to make sure it works before using reverse.Ersatz
B
1

I needed to get this working with force authentication, and finally got it, here is what my test case looks like:

from django.test import TestCase
from rest_framework.test import APIRequestFactory
from django.db.models.query import QuerySet
from rest_framework.test import force_authenticate
from django.contrib.auth.models import User

from config_app.models import Config
from config_app.apps import ConfigAppConfig
from config_app.views import ConfigViewSet

class ViewsTestCase(TestCase):
    def setUp(self):
        # Create a test instance
        self.config = Config.objects.create(
            ads='{"frequency": 1, "site_id": 1, "network_id": 1}',
            keys={}, methods={}, sections=[], web_app='{"image": 1, "label": 1, "url": 1}',
            subscriptions=[], name='test name', build='test build', version='1.0test', device='desktop',
            platform='android', client_id=None)

        # Create auth user for views using api request factory
        self.username = 'config_tester'
        self.password = 'goldenstandard'
        self.user = User.objects.create_superuser(self.username, '[email protected]', self.password)

    def tearDown(self):
        pass

    @classmethod
    def setup_class(cls):
        """setup_class() before any methods in this class"""
        pass

    @classmethod
    def teardown_class(cls):
        """teardown_class() after any methods in this class"""
        pass

    def shortDescription(self):
        return None


    def test_view_set1(self):
        """
        No auth example
        """
        api_request = APIRequestFactory().get("")
        detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
        response = detail_view(api_request, pk=self.config.pk)
        self.assertEqual(response.status_code, 401)

    def test_view_set2(self):
        """
        Auth using force_authenticate
        """
        factory = APIRequestFactory()
        user = User.objects.get(username=self.username)
        detail_view = ConfigViewSet.as_view({'get': 'retrieve'})

        # Make an authenticated request to the view...
        api_request = factory.get('')
        force_authenticate(api_request, user=user)
        response = detail_view(api_request, pk=self.config.pk)
        self.assertEqual(response.status_code, 200)

I'm using this with the django-nose test runner and it seems to be working well. Hope it helps those that have auth enabled on their viewsets.

Bedford answered 7/2, 2019 at 17:48 Comment(0)
P
0

I created a package just for that:

drf-api-action

drf-api-action elevates DRF testing with the api-action decorator, simplifying REST endpoint testing to a seamless, function-like experience.

from drf_api_action.decorators import action_api
from drf_api_action.mixins import APIRestMixin
from rest_framework.viewsets import ModelViewSet

class DummyView(APIRestMixin, ModelViewSet):
    queryset = DummyModel.objects.all()
    serializer_class = DummySerializer

    @action_api(detail=True, methods=["get"], serializer_class=DummySerializer)
    def dummy(self, request, **kwargs):
        serializer = self.get_serializer(instance=self.get_object())
        return Response(data=serializer.data, status=status.HTTP_200_OK)


def test_dummy():
    api = DummyView()
    result = api.dummy(pk=1)
    assert result['dummy_int'] == 1
Posy answered 20/12, 2023 at 20:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.