Skip to main content

How to write test cases for Your Serializers

Lets consider an 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 sets 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 assertIns 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 Decimals and floats: 
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.

Popular posts from this blog

How to read or extract text data from passport using python utility.

Hi ,  Lets get start with some utility which can be really helpful in extracting the text data from passport documents which can be images, pdf.  So instead of jumping to code directly lets understand the MRZ, & how it works basically. MRZ Parser :                 A machine-readable passport (MRP) is a machine-readable travel document (MRTD) with the data on the identity page encoded in optical character recognition format Most travel passports worldwide are MRPs.  It can have 2 lines or 3 lines of machine-readable data. This method allows to process MRZ written in accordance with ICAO Document 9303 (endorsed by the International Organization for Standardization and the International Electrotechnical Commission as ISO/IEC 7501-1)). Some applications will need to be able to scan such data of someway, so one of the easiest methods is to recognize it from an image file. I 'll show you how to retrieve the MRZ infor...

How to generate class diagrams pictures in a Django/Open-edX project from console

A class diagram in the Unified Modeling Language ( UML ) is a type of static structure diagram that describes the structure of a system by showing the system’s classes, their attributes, operations (or methods), and the relationships among objects. https://github.com/django-extensions/django-extensions Step 1:   Install django extensions Command:  pip install django-extensions Step 2:  Add to installed apps INSTALLED_APPS = ( ... 'django_extensions' , ... ) Step 3:  Install diagrams generators You have to choose between two diagram generators: Graphviz or Dotplus before using the command or you will get: python manage.py graph_models -a -o myapp_models.png Note:  I prefer to use   pydotplus   as it easier to install than Graphviz and its dependencies so we use   pip install pydotplus . Command:  pip install pydotplus Step 4:  Generate diagrams Now we have everything installed...

How to Remove course from Open-edX

Go to vagrant  => 1. In the edx-platform directory:  - cd /edx/app/edxapp/edx-platform 2. Run the following Django management command:   - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp lms dump_course_ids --settings aws    - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp lms dump_course_ids --settings=devstack 3. Find the course ID which you'd like to delete in the resulting list of course IDs. 4. Copy the course ID into the following command and run it:  - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp cms delete_course <COURSE_ID> --settings aws  -   sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp cms delete_course <COURSE_ID> --settings=devstack  - You'll be asked to verify the deletion . To verify the deletion, run the command from step 2 above and ensure that the course ID is not in the list. Help reference : ...