Mastering DRF Serializers: Best Practices and Testing Strategies

Filipe Ximenes
September 25, 2023

Django Rest Framework (DRF) serializers are a fundamental component in the world of building robust and efficient RESTful APIs using Django. These serializers act as the bridge between complex data types, such as Django models, and easily renderable JSON or XML. 

Testing serializers is paramount for building robust and reliable RESTful APIs in Django. In this post, I will show the whats and whys of testing Django REST Framework serializers, guiding you through the best practices and strategies for thorough serializer testing. 

Ready to start? So let's begin with some context. 

What Are DRF Serializes For And Why Should They Tested?

At their core, DRF serializers are responsible for two primary tasks:

  • Validation: They validate the incoming data, ensuring that it adheres to the expected structure and constraints, preventing the introduction of erroneous or malicious data into your application.
  • Serialization/Deserialization: Serializers facilitate the conversion of complex data types, like Django models or Python dictionaries, into a format that can be easily transmitted over the web, typically JSON or XML. They also handle the reverse process of deserialization, converting incoming data back into Python data structures.

Testing DRF serializers is crucial for maintaining the reliability, consistency, and data integrity of your RESTful APIs in Django. It not only helps you catch and prevent errors but also serves as documentation and a safety net as your project evolves. By following best practices in serializer testing, you can build APIs that are robust, maintainable, and resilient to unexpected data issues.

Setting Up the Testing Environment

Before diving into testing Django Rest Framework (DRF) serializers, it's crucial to set up a reliable testing environment. This ensures that your tests are conducted in an isolated and controlled environment, minimizing external factors that could affect their outcomes.

Prerequisites for Testing DRF Serializers:

  • Django and DRF Installations: To begin, ensure you have Django and Django Rest Framework (DRF) installed in your development environment. Django provides the foundation for your web application, while DRF extends it to create RESTful APIs. Testing serializers is a natural part of this process, as they play a pivotal role in API development.
  • Django Installation: You can install Django using pip, Python's package manager, with the command [.code-inline]pip install django[.code-inline].
  • DRF Installation: Likewise, DRF can be installed with [.code-inline]pip install djangorestframework[.code-inline].
  • Database Setup: Depending on your project, you may need a database system. Django supports various databases, and you should configure your database settings to work with your specific project requirements. SQLite is often used for testing purposes due to its simplicity.

Creating a virtual environment is a best practice for isolating your project's dependencies and avoiding conflicts with other Python projects on your system. Here's why it's essential for testing DRF serializers:

  • Isolation: A virtual environment allows you to maintain a separate and controlled Python environment for your project. That means the packages and libraries used for testing are isolated from your system-wide Python installation.
  • Dependency Management: You can easily manage and track project-specific dependencies. That is vital because different projects may require different versions of packages, and virtual environments help you avoid version conflicts.
  • Reproducibility: Virtual environments make it easier to recreate your project's environment on another system or share it with collaborators. That ensures your tests run consistently on various development machines.

To create a virtual environment, you can use Python's built-in [.code-inline]venv[.code-inline] module or other tools like [.code-inline]virtualenv[.code-inline]. Once your virtual environment is set up and activated, you can write and run tests for your DRF serializers within a controlled environment, ensuring a reliable and consistent testing process.

Mastering DRF Serializer Testing: A Comprehensive Example

Now, let's delve into a real-life example where I'll explore the what and why of testing Django REST Framework (DRF) serializers. But before we dive in, let's set the stage with some context. Below is the model setup we'll be using for this illustrative example:

 
from django.db import models

class Bike(models.Model):
    COLOR_OPTIONS = (('yellow', 'Yellow'), ('red', 'Red'), ('black', 'Black'))

    color = models.CharField(max_length=255, null=True, blank=True,
                             choices=COLOR_OPTIONS)
    size = models.DecimalField(max_digits=4, decimal_places=2, null=True, blank=True)

And this is the serializer we're testing:

 
from rest_framework import serializers
from bikes.models import Bike

class BikeSerializer(serializers.ModelSerializer):
    COLOR_OPTIONS = ('yellow', 'black')

    color = serializers.ChoiceField(choices=COLOR_OPTIONS)
    size = serializers.FloatField(min_value=30.0, max_value=60.0)

    class Meta:
        model = Bike
        fields = ['color', 'size']

We'll be using the common [.code-inline]unittest[.code-inline] framework, and here's my typical setup:

 
def setUp(self):
        self.bike_attributes = {
            'color': 'yellow',
            'size': Decimal('52.12')
        }

        self.serializer_data = {
            'color': 'black',
            'size': 51.23
        }

        self.bike = Bike.objects.create(**self.bike_attributes)
        self.serializer = BikeSerializer(instance=self.bike)

Now, let's move forward. The first test we'll tackle is essential, as it verifies whether the serializer possesses the exact attributes we expect:

 
def test_contains_expected_fields(self):
    data = self.serializer.data
    self.assertEqual(set(data.keys()), set(['color', 'size']))

We use sets here to ensure that the serializer's output precisely matches the expected keys. This approach is crucial as it detects any field additions or removals from the serializer, guaranteeing the presence of fields.

Next up, we validate whether the serializer generates the expected data for each field, starting with the [.code-inline]color[.code-inline] field:

 
def test_color_field_content(self):
    data = self.serializer.data
    self.assertEqual(data['color'], self.bike_attributes['color'])

For this assertion, we use [.code-inline]self.bike_attributes['color'][.code-inline] to maintain consistency with our default setup and accommodate changes to the global test setup without compromising test quality.

Moving on to the [.code-inline]size[.code-inline] attribute, which differs between the model and serializer with a [.code-inline]DecimalField[.code-inline] in the model and a [.code-inline]FloatField[.code-inline] in the serializer:

 
def test_size_field_content(self):
    data = self.serializer.data
    self.assertEqual(data['size'], float(self.bike_attributes['size']))

A word of caution: when comparing [.code-inline]Decimals[.code-inline] and [.code-inline]floats[.code-inline], careful testing is required, as demonstrated above.

For the [.code-inline]size[.code-inline] attribute, which has both lower and upper bounds, we test edge cases:

 
def test_size_lower_bound(self):
    self.serializer_data['size'] = 29.9
    serializer = BikeSerializer(data=self.serializer_data)
    self.assertFalse(serializer.is_valid())
    self.assertEqual(set(serializer.errors), set(['size']))

We leverage the valid data in self.serializer_data to focus solely on modifying the size attribute to an invalid value, ensuring accurate evaluation.

The upper bound test for the size attribute follows a similar path:

 
def test_size_upper_bound(self):
    self.serializer_data['size'] = 60.1
    serializer = BikeSerializer(data=self.serializer_data)
    self.assertFalse(serializer.is_valid())
    self.assertEqual(set(serializer.errors), set(['size']))

Given the data type discrepancy between the model and serializer for the size attribute, this aspect requires careful testing.

Lastly, we ensure that inputting a float to the serializer correctly converts it to a Decimal value in the model:

 
def test_float_data_correctly_saves_as_decimal(self):
    self.serializer_data['size'] = 31.789
    serializer = BikeSerializer(data=self.serializer_data)
    serializer.is_valid()
    new_bike = serializer.save()
    new_bike.refresh_from_db()
    self.assertEqual(new_bike.size, Decimal('31.79'))

Pay special attention to new_bike.refresh_from_db(), as it ensures the bike.size remains a Decimal and not a float.

Lastly, we confirm whether the serializer appropriately handles invalid choices:

 
def test_color_must_be_in_choices(self):
    self.bike_attributes['color'] = 'red'
    serializer = BikeSerializer(instance=self.bike, data=self.bike_attributes)
    self.assertFalse(serializer.is_valid())
    self.assertEqual(set(serializer.errors.keys()), set(['color']))

That's a wrap for now. Keep in mind that tests can vary significantly based on the application context, but these cases can offer insights and ideas adaptable to your project. 

If you want to learn more, check out our Classy Django REST Framework, a web-based documentation with flattened information about Django REST Framework's class-based views and serializers.