Django REST framework extension
Serializers
BaseModelSerializer
This serializers extends the restframework ModelSerializer
but ensures that the clean()
method in the model is
called. By default, only django forms call the model-level clean, not the serializers.
# models.py
class MyModel(models.Model):
...
def clean():
# here lives the model-level validation
...
# serializers.py
class MyModelSerializer(ModelSerializer)
class Meta:
model = MyModel
# do your stuff here!
Just derive your serializer from the BaseModelSerializer
and you are good to go!
CommonInfoSerializer
In addition to the CommonInfo
model class which provides a neat way to set the creator and last editor of any
database object, this serializers takes care of setting those fields if a request user was found.
Furthermore, it extends the BaseModelSerializer
to ensure that model-level validation is called on serializer
validation.
# models.py
class MyOwnershipRelevantModel(CommonInfo):
...
# serializers.py
class MyOwnershipRelevantModelSerializer(CommonInfoSerializer)
class Meta:
model = MyOwnershipRelevantModel
# do your stuff here!
Fields
RecursiveField
In some cases you’ll have to define a foreign key from and to the same model. One popular use-case is a simple tree
structure. If you want to use the same serializer for the parent and all children, you can utilise our RecursiveField
:
class MyModelSerializer(serializers.ModelSerializer):
...
children = RecursiveField(many=True)
class Meta:
model = MyModel
fields = [
'id',
...
'children',
]
Testing of ViewSets
If you want to test ViewSets
you can use the handy mixin BaseViewSetTestMixin
from this package. It encapsulates
common use-cases and provides a wrapper to be able to easily focus on your tests and not the syntax of how to call a
viewset manually.
Setup
It is easy to use this mixin, just follow these steps. You will probably have some base test class to encapsulate your general test setup. So create a new base api test class like this:
class BaseApiTest(BaseViewSetTestMixin, BaseTest):
def get_default_api_user(self) -> AbstractUser:
return baker.make_recipe('apps.account.tests.user')
Note that the content of get_default_api_user()
is just a suggestion. You have to provide the method, but it is
totally up to you how you generate a base request user.
The idea behind this method is, that you define a base user for your API requests. You still have to pass the user
manually to all of your tests in the execute_request()
method. We did not set this user as a default because the user
is often an essential part of the test and should be selected consciously and with care.
If you do not need this default user, simply return None
in the described method.
Test that authentication is required for an endpoint
Permissions are one of the most vital things to test in your application. So this mixin provides a helper method, so you are able to test what you require without thinking about how to test it.
class MyFancyViewsetApiTest(BaseApiTest):
def test_list_authentication_required(self):
self.validate_authentication_required(
url=reverse('my-fancy-viewset-list'),
method='get',
view='list',
)
Testing CRUD views and custom actions
Here is an example of how to test a CRUD action. The helper method execute_request()
encapsulates all necessary logic
to create an API response. This response can then be asserted. It is wise to always at first assert the status code. In
this case, if the API does not return valid data, but an error code, the reason of failure is obvious. If you on the
other hand start with asserting response data, you might get an IndexError
which will point the assigned developer
in the wrong direction at first.
class MyFancyViewsetApiTest(BaseApiTest):
def test_list_regular(self):
response = self.execute_request(
method='get',
url=reverse('my-fancy-viewset-list'),
viewset_kwargs={'get': 'list'},
user=self.user,
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 3)
Notice that you need to tell the viewset which CRUD view you want to test. Here is a list of valid default parameters
you can pass to viewset_kwargs
:
Name |
Key |
Value |
---|---|---|
List |
get |
list |
Detail |
get |
retrieve |
Create |
post |
create |
Update |
put |
update |
Delete |
delete |
destroy |
The “Key” is the HTTP method to be used, and the value is the name of the action. If you have a custom action, just choose the corresponding method and pass the action name as “value”.
Validate that a CRUD method is deactivated
It might be vital for your application that some CRUD (create, retrieve, update, delete) views are exposed with a given
viewset but others are not. It happens very easily that a generic DRF mixin is copy-and-pasted and opens an unwanted
list
, retrieve
, create
, etc. endpoint. That is why we recommend testing if all unwanted CRUD methods are
deactivated.
With this mixin you can ensure this easily like this:
class MyFancyViewsetApiTest(BaseApiTest):
view_class = views.MyFancyViewset
...
def test_create_not_activated(self):
with self.assertRaises(AttributeError):
self.execute_request(
method='post',
url=reverse('my-fancy-viewset-list'),
viewset_kwargs={'post': 'create'},
user=self.user,
)
Testing file uploads
Here is an example of how to test file uploads. Feel free to adjust the data
kwarg to suit your needs.
from django.core.files.uploadedfile import SimpleUploadedFile
class MyFancyViewsetApiTest(BaseApiTest):
view_class = views.MyFancyViewset
...
def test_file_upload_action(self):
response = self.execute_request(
method='post',
url=reverse('my-fancy-viewset-list'),
viewset_kwargs={'post': 'my-custom-action'},
user=self.user,
data={'file': SimpleUploadedFile("file.txt", b"some fancy content")},
data_format='multipart',
)
self.assertEqual(response.status_code, status.HTTP_200_OK)