article
How I test my DRF serializers
Filipe Ximenes • 11 January 2017
In this blog post, I will show the whats and whys on testing Django REST Framework serializers. First, some context. Here is the model setup we are going to use for this 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 I'm are going to test:
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']
I'm going to use the common unittest
framework, so here is the setup I normally go with:
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)
First, notice I have a default set of attributes (self.bike_attributes
) that I'll use to initialize a Bike
object. The self.serializer_data
is also a set of attributes but this time to be used as default data
parameters to the serializer when we need them. I always set those as valid values [more on this latter]. The last bit is the self.serializer
which is a simple instance of the serializer initialized with the self.bike
object. The reason why I'm defining those in the setup and setting them as attributes to the test class is because they will be repeatedly used in the tests so we can skip setting them up every time (and some other reasons I'll be talking about along the post).
Let's get started.
The first test is actually one of the most important. It verifies if the serializer has the exact attributes it is expected to.
def test_contains_expected_fields(self):
data = self.serializer.data
self.assertEqual(set(data.keys()), set(['color', 'size']))
I'm using set
s to make sure that the output from the serializer has the exact keys I expect it to. Using a set
to make this verification is actually very important because it will guarantee that the addition or removal of any field to the serializer will be noticed by the tests. Verifying the presence of the field using a series of assertIn
s would pick the removal of a field but not additions.
Update
As highlighted by Aki in the comments, self.assertItemsEqual(data.keys(), ['color', 'size'])
can also be used and is more readable than self.assertEqual(set(data.keys()), set(['color', 'size']))
. If you are using Python 3 [and you should be], assertItemsEqual
is now called assertCountEqual
.
Now moving on to check if the serializer produces the expected data to each field. The color
field is pretty standard:
def test_color_field_content(self):
data = self.serializer.data
self.assertEqual(data['color'], self.bike_attributes['color'])
Notice I'm using self.bike_attributes['color']
to make the assert. Because those default attributes are set in the setUp
and not inside the test it's a good idea to also assert using them, this will allow changes in the global test set up that will not interfere with the tests without compromising the quality of the suit.
Next to the size
attribute. This one is a little more tricky because the model is using a DecimalField
while the corresponding attribute in the serializer uses a FloatField
.
def test_size_field_content(self):
data = self.serializer.data
self.assertEqual(data['size'], float(self.bike_attributes['size']))
CAUTION
Be careful when comparing Decimal
s and float
s:3.14 == float(Decimal('3.14'))
-> True
Decimal(3.14) == Decimal('3.14')
-> False
size
attribute has both lower and upper bounds so it's very important to 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']))
Remember I said self.serializer_data
should be valid values? That's the point where I'll take advantage of this. Because I know the default data in self.serializer_data
is valid I can change only the value of the size
to an invalid value. This will guarantee the test will be picking up on the exact behavior it was meant to. The self.assertEqual(set(serializer.errors), set(['size']))
assert also reinforces this.
Upper bound size
test goes the same way:
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']))
Because the size
data type changes from model to serializer, it needs to be carefully tested. The conversion from model DecimalField
to float was previously tested but inputting a float to the serializer and converting it to a correct Decimal value in the model is not yet covered.
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'))
Special attention to new_bike.refresh_from_db()
, if you don't do this bike.size
will be a float
and not a Decimal
. Notice I set the self.serializer_data['size']
to 31.789
. By doing this I'm also verifying that it is correctly rounded (Decimal('31.79')
).
Lastly, because the choices options are different between model and serializer, it's good to check if the serializer is picking on invalid values:
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 it for now. Tests vary a lot according to the application context, but I hope the cases exposed in this post can give you some insights or ideas that you can adapt to your project. As usual, feedbacks are very welcome, leave them in the comments. I'll be happy to make updates if you can spot things I'm missing.
Looking for more?
cdrf.co lets you explore DRF views and serializers
Python API clients with Tapioca
comments