Skip to main content

Declarative REST API Tests with Django.

Project description

Django REST Testing

Tests Coverage PyPI version Downloads License MyPy Ruff

Django REST Testing

Django REST Testing is a small, declarative, opinionated, and yet powerful tool designed to streamline the development of tests for RESTful endpoints within Django. This package embraces best practices to ensure efficient and robust endpoint testing, allowing developers to focus on what truly matters when testing their applications: ensuring they work as expected.

Originally integrated within Django Ninja CRUD, it has evolved into a standalone package. This evolution enables developers to test their RESTful endpoints with ease and precision, regardless of the framework in use.

By using a scenario-based test case approach, this package empowers developers to rigorously test RESTful endpoints under varied conditions and inputs. Each scenario specifically targets distinct endpoint behaviors—ranging from handling valid and invalid inputs to managing nonexistent resources and enforcing business rules.

This modular approach breaks tests into distinct, manageable units, streamlining the testing process, enhancing clarity and maintainability, and ensuring comprehensive coverage — making it an indispensable tool for modern web development.

📝 Requirements

Python versions Django versions Pydantic versions

⚒️ Installation

pip install django-rest-testing

For more information, see the installation guide.

👨‍🎨 Example

Let's imagine you're building a system for a university and you have a model called Department. Each department in your university has a unique title.

# examples/models.py
import uuid
from django.db import models

class Department(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=255, unique=True)

To interact with this data, we need a way to convert it between Python objects and a format that's easy to read and write (like JSON). We can use Pydantic to define schemas for our data:

# examples/schemas.py
import uuid
from pydantic import BaseModel

class DepartmentIn(BaseModel):
    title: str

class DepartmentOut(BaseModel):
    id: uuid.UUID
    title: str

The DepartmentIn schema defines what data we need when creating or updating a department. The DepartmentOut schema defines what data we'll provide when retrieving a department.

Now, we take pride in the simplicity and directness of using vanilla Django to handle our endpoints. It’s like cooking a gourmet meal with just a few basic ingredients — surprisingly satisfying and impressively functional.

# examples/views.py
import uuid

from django.http import HttpRequest, HttpResponse
from django.views.decorators.http import require_http_methods

from examples.models import Department
from examples.schemas import DepartmentIn, DepartmentOut


@require_http_methods(["GET", "PUT", "DELETE"])
def read_update_delete_department(request: HttpRequest, id: uuid.UUID):
    department = Department.objects.get(id=id)

    if request.method == "GET":
        response_body = DepartmentOut.model_validate(department, from_attributes=True)
        return HttpResponse(content=response_body.model_dump_json(), status=200)

    elif request.method == "PUT":
        request_body = DepartmentIn.model_validate_json(request.body)
        for key, value in request_body.dict().items():
            setattr(department, key, value)

        department.full_clean()
        department.save()
        response_body = DepartmentOut.model_validate(department, from_attributes=True)
        return HttpResponse(content=response_body.model_dump_json(), status=200)

    elif request.method == "DELETE":
        department.delete()
        return HttpResponse(content=b"", status=204)

There you have it—a minimalistic yet powerful approach to handling RESTful operations in Django. Up next, let’s dive into how declarative testing makes validating these endpoints both efficient and straightforward.

# examples/tests.py
import uuid

from examples.models import Department
from examples.schemas import DepartmentOut

from rest_testing import APITestCase, APIViewTestScenario


class TestDepartmentViewSet(APITestCase):
    department_1: Department
    department_2: Department

    @classmethod
    def setUpTestData(cls):
        cls.department_1 = Department.objects.create(title="department-1")
        cls.department_2 = Department.objects.create(title="department-2")

    def test_read_department(self):
        self.assertScenariosSucceed(
            method="GET",
            path="/api/departments/{id}",
            scenarios=[
                APIViewTestScenario(
                    path_parameters={"id": self.department_1.id},
                    expected_response_status=200,
                    expected_response_body_type=DepartmentOut,
                    expected_response_body={
                        "id": str(self.department_1.id),
                        "title": self.department_1.title,
                    },
                ),
                APIViewTestScenario(
                    path_parameters={"id": uuid.uuid4()},
                    expected_response_status=404,
                ),
            ],
        )

    def test_update_department(self):
        self.assertScenariosSucceed(
            method="PUT",
            path="/api/departments/{id}",
            scenarios=[
                APIViewTestScenario(
                    path_parameters={"id": self.department_1.id},
                    request_body={"title": "new_title"},
                    expected_response_status=200,
                    expected_response_body_type=DepartmentOut,
                    expected_response_body={
                        "id": str(self.department_1.id),
                        "title": "new_title",
                    },
                ),
                APIViewTestScenario(
                    path_parameters={"id": uuid.uuid4()},
                    request_body={"title": "new_title"},
                    expected_response_status=404,
                ),
                APIViewTestScenario(
                    path_parameters={"id": self.department_1.id},
                    request_body={"title": [1]},
                    expected_response_status=400,
                ),
                APIViewTestScenario(
                    path_parameters={"id": self.department_1.id},
                    request_body={"title": self.department_2.title},
                    expected_response_status=400,
                ),
            ],
        )

    def test_delete_department(self):
        self.assertScenariosSucceed(
            method="DELETE",
            path="/api/departments/{id}",
            scenarios=[
                APIViewTestScenario(
                    path_parameters={"id": self.department_1.id},
                    expected_response_status=204,
                    expected_response_body=b"",
                ),
                APIViewTestScenario(
                    path_parameters={"id": uuid.uuid4()},
                    expected_response_status=404,
                ),
            ],
        )

As you can see, the APITestCase class provides a simple and intuitive way to define test scenarios. Each scenario specifies the expected request and response, making it easy to understand what's being tested. This approach not only simplifies the testing process but also enhances the clarity and maintainability of test suites.

📚 Documentation

For more information, see the documentation.

🫶 Support

First and foremost, a heartfelt thank you for taking an interest in this project. If it has been helpful to you or you believe in its potential, kindly consider giving it a star on GitHub. Such recognition not only fuels my drive to maintain and improve this work but also makes it more visible to new potential users and contributors.

GitHub Repo stars

If you've benefited from this project or appreciate the dedication behind it, consider showing further support. Whether it's the price of a coffee, a word of encouragement, or a sponsorship, every gesture adds fuel to the open-source fire, making it shine even brighter. ✨

Sponsor Buy me a coffee

Your kindness and support make a world of difference. Thank you! 🙏

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_rest_testing-0.1.0.tar.gz (10.6 kB view details)

Uploaded Source

Built Distribution

django_rest_testing-0.1.0-py3-none-any.whl (9.1 kB view details)

Uploaded Python 3

File details

Details for the file django_rest_testing-0.1.0.tar.gz.

File metadata

  • Download URL: django_rest_testing-0.1.0.tar.gz
  • Upload date:
  • Size: 10.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.12.3 Linux/6.5.0-1018-azure

File hashes

Hashes for django_rest_testing-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ee67df498e38d129d425fe878c8640a33de2e184e8d350617edbea0448903a95
MD5 f78fad9fb2b22689ceee3e5450637931
BLAKE2b-256 190ae357ad55925d6ea43d2a01afd20281a5b4bcab3f3ede56ca0c51ebdb75f1

See more details on using hashes here.

File details

Details for the file django_rest_testing-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_rest_testing-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fcf2a8c0b384da37146042ca1724021f24d1d60ecc3b23976d9c57cb9b7779f
MD5 b47b2658c167023e9844382821bd910c
BLAKE2b-256 2f81d612e51bdb98f5a2145716d1ad87291545ec44bd3c47d75a61fdd19db116

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page