It’s not because things are difficult that we dare not venture. It’s because we dare not venture that they are difficult. - Seneca
A section of Django REST framework (DRF) documentation is dedicated to testing your API views. It has a lot of information about the different tools the library is providing and code examples on how to use them. I was asked how to test an API call by mimicking a request, so here is how I usually do it in my projects.
The serializer
First, we have a basic model for a home which fields describe its address. BaseModel
is an abstract class that adds a UUID
, a created_at
and a modified_at
fields to my objects:
class Home(BaseModel):
class SupportedCountries:
CAN = "CANADA"
US = "UNITED_STATES"
CHOICES = (
(CAN, _("Canada")),
(US, _("United States")),
)
name = models.CharField(max_length=50, default="Home")
address_line_1 = models.CharField(max_length=500)
address_line_2 = models.CharField(
max_length=500, blank=True, verbose_name=_("Address line 2 (optional)")
)
city = models.CharField(max_length=100)
state_province = models.CharField(max_length=50, verbose_name=_("State/Province"))
zip_code_postal_code = models.CharField(
max_length=7, verbose_name=_("Zip code/Postal code")
)
country = models.CharField(
max_length=15,
choices=SupportedCountries.CHOICES,
default=SupportedCountries.US,
)
def __str__(self):
return self.name
The resulting serializer is thus very simple with no inner serializer. We declare all the fields from the model plus the id
field from the BaseModel
abstract class:
class HomeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Home
fields = [
"id",
"name",
"address_line_1",
"address_line_2",
"city",
"state_province",
"zip_code_postal_code",
"country",
]
The test class
To generate objects based on my models for my tests, I use factory boy and I populate the fields with fake data using faker:
import factory
from factory.django import DjangoModelFactory
class HomeFactory(DjangoModelFactory):
name = "Home"
address_line_1 = factory.Faker("street_address")
address_line_2 = factory.Faker("building_number")
city = factory.Faker("city")
state_province = factory.Faker("state")
zip_code_postal_code = factory.Faker("postcode")
country = models.Home.SupportedCountries.US
class Meta:
model = models.Home
I usually use Django TestCase class for testing purposes, which is a subclass of Python unittest.TestCase. It has a client to authenticate a user so you could test mixins and permission behaviors. DRF has a class called APITestCase that extends Django TestCase
with the same functions but using APIClient which allows one to authenticate as an API user.
class TestHomeAPIView(APITestCase):
def setUp(self):
self.url = reverse("api:home_view") # use the view url
self.home = factories.HomeFactory()
user = factories.UserFactory()
self.client.force_authenticate(user=user)
def test_get(self):
response = self.client.get(self.url)
response.render()
self.assertEquals(200, response.status_code)
expected_content = {
"id": str(self.home.id),
"address_line_1": self.home.address_line_1,
"address_line_2": self.home.address_line_2,
"city": str(self.home.city),
"state_province": str(self.home.state_province),
"zip_code_postal_code": str(self.home.zip_code_postal_code),
"country": str(self.home.country),
}
self.assertListEqual(expected_content, json.loads(response.content))
Let's dive into that test code and explain what it does. First we have the setUp
function which is common to TestCase and define code that will be run before every function prefixed with test_
in the class. I define the url for my view and a home
object that will be recreated each time, making my test deterministic. I then create a user with a factory that gives the permissions I need for testing the view. The last line is really important, even more so for an API view with authentication and permission mechanisms. By default, the DEFAULT_PERMISSION_CLASSES
array contains rest_framework.permissions.IsAuthenticated
so all my views require to be logged in as a user. We authenticate the user with the APIClient
so we can make calls to the view and the permissions can be evaluated.
I then declare test_get
to make a simple GET
test of my view. I first capture the call response into a variable of the same name. At first, I assumed response.content
would be available, but because of template rendering mechanism internal to Django, it is not. We need to manually declare the rendering. Then I assert that my response was successful and generated an HTTP 200 that means everything went fine. The next line declares an expected_content
variable which is dict containing all the fields of my home
object. Lastly, I parse a JSON object to a Python object using json.loads
, which I compare to my expected content to make sure what I received is correct.
Wrapping up
Django REST Framework has a great documentation with a thorough section on tests. We went through some bits of code for the model, the serializer and finally the test. This test was written à la Django using factory boy and Faker, and leveraging DRF APIClient
contained in APITestCase
testing class.
We went from start to finish of my testing process for an API view. There might be some portions that could be optimized, but it proved to be reliable for me.
Please share with me any tips or amazing bits of code that make your testing experience with Django REST Framework easier. If you spot an error, do not hesitate to leave a comment.
Top comments (2)
I liked your article!
Im inviting you to use my new package for rest framework called drf-api-action
which is designed to elevate your testing experience for (DRF) REST endpoints. this package empowers you to effortlessly test your REST endpoints as if they were conventional functions.
Features:
Simplified Testing: Testing DRF REST endpoints using the api-action decorator, treating them like regular functions.
Seamless Integration: Replacing DRF's action decorator with api-action in your WebViewSet seamlessly.
Clear Traceback: Instead of getting a response with error code, get the real traceback that led to the error.
Pagination Support: Paginating easily through pages by a single kwarg.
github.com/Ori-Roza/drf-api-action
thanks!
I stumbled on your project a couple of weeks ago. I will definitely try and see how I can leverage it in my projects. Thanks!