Skip to content

Intro

Welcome to the world of testing! Testing is a crucial part of software development, and it’s great that you’re diving into it. Here's a quick guide on testing in Django and Django REST Framework (DRF), along with some information on test-driven development (TDD).

Testing in Django

  1. Understand Django’s Testing Framework: Django comes with a built-in testing framework that extends Python's unittest module. This framework allows you to create test cases for your Django applications.

  2. Create Test Cases:

    • Unit Tests: Test individual components, like models and functions, in isolation.
    • Integration Tests: Test how different components work together, like testing views and their interaction with the database.
    from django.test import TestCase
    from .models import MyModel
    
    class MyModelTestCase(TestCase):
        def setUp(self):
            MyModel.objects.create(name="test")
    
        def test_model_str(self):
            obj = MyModel.objects.get(name="test")
            self.assertEqual(str(obj), "test")
    
  3. Use Django’s Test Client: For testing views, you can use Django’s TestClient to simulate requests and inspect responses.

    from django.test import TestCase, Client
    
    class MyViewTestCase(TestCase):
        def setUp(self):
            self.client = Client()
    
        def test_view_status_code(self):
            response = self.client.get('/my-url/')
            self.assertEqual(response.status_code, 200)
    
  4. Run Your Tests: Use the following command to run your tests:

    python manage.py test
    

Testing in Django REST Framework (DRF)

  1. Use DRF’s Testing Tools: DRF provides a set of tools for testing APIs, including the APITestCase class.

    from rest_framework.test import APITestCase
    from rest_framework import status
    
    class MyAPITestCase(APITestCase):
        def test_get_endpoint(self):
            response = self.client.get('/api/my-endpoint/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
    
  2. Test Your API Endpoints:

    • GET Requests: Test retrieving data.
    • POST Requests: Test creating new records.
    • PUT/PATCH Requests: Test updating existing records.
    • DELETE Requests: Test deleting records.
    from rest_framework import status
    from rest_framework.test import APITestCase
    
    class MyAPITestCase(APITestCase):
        def test_create_record(self):
            data = {'name': 'test'}
            response = self.client.post('/api/my-endpoint/', data, format='json')
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    

Test-Driven Development (TDD)

In TDD, you write your tests before writing the actual code. Here’s a simplified process:

  1. Write a Test: Start by writing a test for a new feature or functionality. Since your code doesn’t exist yet, this test will fail initially.

  2. Write the Code: Implement the minimal amount of code needed to make the test pass.

  3. Run the Test: Run the test suite to see if your new test passes.

  4. Refactor: Clean up your code while ensuring all tests continue to pass.

  5. Repeat: Continue this cycle for each new feature or bug fix.

Summary

  • For Django: Use TestCase for unit and integration tests. Utilize Django’s test client for view tests.
  • For DRF: Use APITestCase for API endpoint testing.
  • TDD: Write tests before the code, ensure they pass, and refactor.

Starting with these basics will give you a strong foundation in testing your Django and DRF applications. As you get more comfortable, you can explore advanced testing techniques and tools. Happy testing!


Folder Structure

Here’s a typical folder structure for a Django project with tests, including both Django and Django REST Framework (DRF) components.

Basic Folder Structure for Django Testing

Here’s a typical folder structure for a Django project:

my_project/
├── my_app/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_models.py
│   │   ├── test_views.py
│   │   ├── test_forms.py
│   │   └── test_utils.py
│   ├── urls.py
│   ├── views.py
│   └── migrations/
│       └── __init__.py
├── my_project/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── requirements.txt

Folder Structure for DRF Testing

When dealing with DRF, your tests directory might have a structure like this:

my_project/
├── my_app/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── views.py
│   ├── serializers.py
│   ├── urls.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── test_models.py
│   │   ├── test_views.py
│   │   ├── test_serializers.py
│   │   ├── test_apis.py
│   │   └── test_permissions.py
│   └── migrations/
│       └── __init__.py
├── my_project/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── requirements.txt

Breakdown of Testing Files

  • tests/__init__.py: Makes the tests directory a Python package. This file can be empty.

  • test_models.py: Contains tests for your Django models, ensuring that the model methods and properties work as expected.

  • test_views.py: Contains tests for your Django views, including both class-based and function-based views. It often uses Django’s test client to simulate requests.

  • test_forms.py: Contains tests for Django forms. Useful for checking form validation and rendering.

  • test_utils.py: Contains tests for utility functions or classes used throughout your app.

  • test_serializers.py: For DRF projects, this file contains tests for serializers to ensure they correctly serialize and deserialize data.

  • test_apis.py: Contains tests for DRF API endpoints, testing various HTTP methods (GET, POST, PUT, DELETE) and ensuring correct responses and status codes.

  • test_permissions.py: Contains tests for custom permissions or authentication mechanisms used in DRF.

Tips for Organizing Tests
  • Group by Feature or Component: Group tests based on what they are testing (e.g., models, views, serializers) to keep related tests together.
  • Keep Test Files Manageable: As your project grows, split large test files into smaller ones to maintain readability and manageability.
  • Use Descriptive Names: Name your test files and functions descriptively to make it clear what functionality they are testing.
  • Follow DRY Principle: Use fixtures, test factories, or utilities to avoid repetitive code in your test cases.

This structure will help you keep your tests organized and ensure you can easily navigate and manage your test cases as your project grows.


Example

  • Objective: Ensure that your model methods, properties, and validations work as expected.
  • Test Cases to Consider:
    • Model field validations.
    • Custom model methods.
    • Model save and delete operations.
# my_app/tests/test_models.py
from django.test import TestCase
from .models import MyModel

class MyModelTestCase(TestCase):
    def setUp(self):
        self.model_instance = MyModel.objects.create(name='test')

    def test_model_str(self):
        self.assertEqual(str(self.model_instance), 'test')

    def test_model_method(self):
        result = self.model_instance.some_method()
        self.assertEqual(result, expected_value)
  • Objective: Verify that views return the correct HTTP responses and handle requests as expected.

  • Test Cases to Consider:

    • HTTP status codes (200 OK, 404 Not Found, 403 Forbidden, etc.).
    • Response content and structure.
    • Permissions and authentication.
# my_app/tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse

class MyViewTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        self.url = reverse('my_view_name')

    def test_get_request(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Expected content')

    def test_post_request(self):
        response = self.client.post(self.url, {'key': 'value'})
        self.assertEqual(response.status_code, 201)
  • Objective: Ensure that forms validate input correctly and handle form submissions properly.
  • Test Cases to Consider:
    • Form validation (both valid and invalid data).
    • Form field requirements and constraints.
# my_app/tests/test_forms.py
from django.test import TestCase
from .forms import MyForm

class MyFormTestCase(TestCase):
    def test_form_validity(self):
        form = MyForm(data={'field': 'value'})
        self.assertTrue(form.is_valid())

    def test_form_invalidity(self):
        form = MyForm(data={'field': ''})
        self.assertFalse(form.is_valid())
  • Objective: Ensure that serializers correctly handle data serialization and deserialization.
  • Test Cases to Consider:
    • Serialization and deserialization of valid and invalid data.
    • Field validations and transformations.
# my_app/tests/test_serializers.py
from rest_framework.test import APITestCase
from .serializers import MyModelSerializer
from .models import MyModel

class MyModelSerializerTestCase(APITestCase):
    def setUp(self):
        self.model_instance = MyModel.objects.create(name='test')
        self.serializer = MyModelSerializer(instance=self.model_instance)

    def test_serializer_valid(self):
        data = {'name': 'test'}
        serializer = MyModelSerializer(data=data)
        self.assertTrue(serializer.is_valid())

    def test_serializer_invalid(self):
        data = {'name': ''}
        serializer = MyModelSerializer(data=data)
        self.assertFalse(serializer.is_valid())
  • Testing APIs (DRF)
  • Objective: Verify that API endpoints return correct responses and handle requests properly.

  • Test Cases to Consider:

    • GET, POST, PUT/PATCH, DELETE requests.
    • Authentication and permissions.
    • Response status codes and data formats.
# my_app/tests/test_apis.py
from rest_framework.test import APITestCase
from rest_framework import status
from .models import MyModel

class MyModelAPITestCase(APITestCase):
    def setUp(self):
        self.url = '/api/my-model/'
        self.model_instance = MyModel.objects.create(name='test')

    def test_get_model_list(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIn('test', response.data[0]['name'])

    def test_create_model(self):
        data = {'name': 'new_model'}
        response = self.client.post(self.url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data['name'], 'new_model')
  • Testing Middleware and Utilities
  • Objective: Ensure that middleware and utility functions perform their intended roles.

  • Test Cases to Consider:

    • Middleware behavior (e.g., modifying requests/responses).
    • Utility function outputs and edge cases.
# my_app/tests/test_utils.py
from django.test import TestCase
from .utils import my_utility_function

class MyUtilityFunctionTestCase(TestCase):
    def test_function_output(self):
        result = my_utility_function('input')
        self.assertEqual(result, 'expected_output')
Best Practices
  • Coverage: Aim to cover critical functionalities, edge cases, and potential failure points. Not every single line needs testing, but ensure key paths are well-tested.
  • Modularity: Write modular tests that focus on one aspect of functionality per test case.
  • Automation: Integrate tests into your CI/CD pipeline to ensure they run automatically with each code change.

By following these guidelines, you ensure that your Django and DRF applications are thoroughly tested and reliable.


unittest module

The unittest module in Python is a built-in module that provides a framework for writing and running tests. It is a part of Python's standard library and is used to create and execute unit tests, which are tests focused on individual units of code (like functions or methods) to ensure they work as expected.

Django and unittest

Django’s testing framework is built on top of Python’s unittest module. This means that while Django provides additional features and conveniences for testing Django-specific components (like models, views, and forms), it still leverages the core functionality of unittest.

Here’s a quick overview of how unittest integrates with Django and how it’s used within Django projects:

Key Features of unittest

  1. Test Case Classes:

    • You create test cases by subclassing unittest.TestCase. Each method within the test case class represents a single test.
    • Django’s django.test.TestCase extends unittest.TestCase and adds additional functionality specific to Django.
    import unittest
    
    class MyTestCase(unittest.TestCase):
        def test_addition(self):
            self.assertEqual(1 + 1, 2)
    
  2. Assertions:

    • The unittest module provides various assertion methods to test conditions in your code, such as assertEqual(), assertTrue(), assertFalse(), and assertRaises().
    • Django’s TestCase inherits these assertions and also provides Django-specific assertions.
    self.assertEqual(a, b)
    self.assertTrue(condition)
    
  3. Test Fixtures:

    • unittest allows you to set up and tear down resources before and after tests using setUp() and tearDown() methods.
    • In Django, django.test.TestCase extends this with additional features, like setting up and tearing down a test database.
    class MyTestCase(unittest.TestCase):
        def setUp(self):
            self.resource = setup_resource()
    
        def tearDown(self):
            teardown_resource(self.resource)
    
  4. Test Suites:

    • You can group multiple test cases into a test suite and run them together.
    • Django’s test runner can discover and execute all tests in a project automatically.
    import unittest
    
    def suite():
        suite = unittest.TestSuite()
        suite.addTest(MyTestCase('test_addition'))
        return suite
    
  5. Running Tests:

    You can run tests from the command line using python -m unittest or, in Django, using python manage.py test.

    python -m unittest discover
    python manage.py test
    

Django’s TestCase and unittest Integration

Django’s TestCase is a subclass of unittest.TestCase with added functionality specific to Django applications:

  1. Database Integration:

    • Django’s TestCase uses a separate test database, ensuring that tests don’t affect your development or production databases.
    • It automatically creates and destroys the test database for each test.
    from django.test import TestCase
    
    class MyModelTestCase(TestCase):
        def setUp(self):
            # Setup code
            pass
    
        def test_model_method(self):
            # Test code
            pass
    
  2. Test Client:

    • Provides a way to simulate HTTP requests and interact with your Django application
    from django.test import TestCase, Client
    
    class MyViewTestCase(TestCase):
        def setUp(self):
            self.client = Client()
    
        def test_get_view(self):
            response = self.client.get('/my-url/')
            self.assertEqual(response.status_code, 200)
    
  3. Additional Assertions:

    • Django’s TestCase includes additional assertions useful for web applications, like assertContains(), assertRedirects(), and assertTemplateUsed().
    from django.test import TestCase
    
    class MyViewTestCase(TestCase):
        def test_view_contains_text(self):
            response = self.client.get('/my-url/')
            self.assertContains(response, 'Expected text')
    

Summary

  • unittest Module: Python’s built-in framework for creating and running tests. It provides a standard way to write tests and includes various assertion methods.
  • Django’s TestCase: Extends unittest.TestCase with Django-specific features, including test database management, test clients, and additional assertions tailored for web applications.

By leveraging unittest and Django’s testing enhancements, you can write effective and comprehensive tests for your Django applications.


Which to choose

  • pytest: Use it for more flexibility, better syntax, and advanced features. It is suitable for any Python project needing sophisticated testing capabilities.
  • pytest-django: Use it when working on Django projects and you want to integrate pytest with Django’s testing utilities and fixtures.
  • pytest-factoryboy: Use it when you need to create complex test data and want to leverage factory_boy with pytest.