Lets consider an example.
Here is the model setup we are going to use for this example:
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,
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 comparingDecimal
s andfloat
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 se 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']))
you can comment or feedback about the blog.