-
Overview of Django Bestpractice
- Where Business Logic Should Reside
- Where Business Logic Should Not Reside
- Why Avoid Putting Business Logic in APIs, Views, Serializers, or Forms?
- Why Avoid Custom Managers and Querysets for Business Logic?
- Why Avoid Signals for Business Logic?
- Model Properties vs Selectors
- Additional Recommendations
- Principle
- Starting with a Cookiecutter Template
-
Testing - General Guidelines for Testing - Testing with Django and DRF - Using Faker for Data Generation - Factories for Advanced Testing - Example: Advanced Usage
The core principles of structuring a Django project can be outlined as follows:
- Services: Functions designed primarily for database write operations, such as creating or updating records.
- Selectors: Functions that focus on retrieving data from the database, often involving complex queries or filtering.
- Model Properties: For straightforward logic related to individual fields, provided they do not introduce significant complexity.
- Model Clean Methods: For performing additional validation that goes beyond field-specific checks, with some exceptions.
- APIs and Views: These should handle request and response processing, leaving business logic to services and selectors.
- Serializers and Forms: These should be used for data validation, transformation, and serialization, not business rules.
- Form Tags: These are meant for rendering HTML forms and should not contain business logic.
- Model Save Methods: Custom save logic should be minimized to keep the model simple and focused on representing data.
- Querysets: These should primarily deal with query operations and not be overloaded with business logic.
- Signals: Signals can lead to hidden dependencies and make the code harder to maintain and debug.
Using APIs, Views, serializers, and forms for business logic can:
- Fragment the Logic: Spreading business logic across multiple layers makes it difficult to trace the flow of data and understand the application’s behavior.
- Hide Details: You may need to delve into the inner workings of these abstractions to make changes, leading to increased complexity and potential errors.
While these components are excellent for simple CRUD operations, real-world applications often involve more complex logic, necessitating a more organized approach.
Custom managers and querysets can be beneficial for exposing better APIs tailored to your domain, but they are not ideal for encapsulating all business logic because:
- Domain Logic: Business logic often spans multiple models and doesn’t always map directly to the data model.
- Cross-Model Operations: Logic that involves multiple models can be challenging to place within a single manager or queryset.
- External Dependencies: Business logic might require interactions with external systems, which should be kept out of custom manager methods.
Instead, separating domain logic into a service layer ensures that your application remains modular and maintainable.
Using signals to manage business logic can quickly lead to issues:
- Implicit Connections: Signals are meant to decouple components, but using them for closely related logic makes these connections implicit and harder to trace.
- Cache Invalidation: While signals can handle cache invalidation well, they should not manage core business logic due to the complexity they introduce.
Reserve signals for specific use cases where decoupling is essential, but avoid them for the primary domain or business logic.
- Complex Relationships: If a property involves traversing multiple relationships, it is better implemented as a selector.
- Performance Concerns: If a property is complex and likely to cause N + 1 query issues when serialized, it should be a selector to ensure efficient data retrieval.
- Separation of Concerns: Ensure that each component of the application has a distinct responsibility, improving maintainability and testability.
- Reusability: Design services and selectors to be reusable across different parts of the application, promoting DRY (Don't Repeat Yourself) principles.
- Testing: Isolate business logic in services and selectors to facilitate unit testing without involving the entire application stack.
- Documentation: Clearly document the purpose and usage of each service and selector to aid understanding and collaboration among team members.
The overarching principle is to "separate concerns" to enhance maintainability, testability, and clarity within the Django project.
To set up a project with a solid structure from the beginning, consider using a cookiecutter template. Here are a few recommendations:
- Styleguide-Example: Use this project as a starting point.
- cookiecutter-django: This template includes a wide range of useful features.
- Custom Cookiecutter: Create a custom template that suits your specific needs and turn it into a reusable cookiecutter project.
Django models should primarily manage the data model and avoid business logic as much as possible.
Creating a BaseModel
to inherit from is a good practice. This base model can include common fields like created_at
and updated_at
. Here’s an example:
from django.db import models
from django.utils import timezone
class BaseModel(models.Model):
created_at = models.DateTimeField(db_index=True, default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
Inheriting from BaseModel
ensures these fields are included in all your models:
class SomeModel(BaseModel):
pass
For additional validation, use the model's clean
method. This method ensures that data meets certain criteria before being saved to the database. For example:
class Course(BaseModel):
name = models.CharField(unique=True, max_length=255)
start_date = models.DateField()
end_date = models.DateField()
def clean(self):
if self.start_date >= self.end_date:
raise ValidationError("End date cannot be before start date")
To invoke the clean
method, call full_clean
before saving an instance:
def course_create(*, name: str, start_date: date, end_date: date) -> Course:
obj = Course(name=name, start_date=start_date, end_date=end_date)
obj.full_clean()
obj.save()
return obj
This approach works well with Django admin, as the forms there trigger full_clean
on the instance.
General Rules for Using clean
- Use clean for validation based on multiple non-relational fields of the model.
- Move complex validation or validation involving multiple models to the service layer.
Leverage Django's model constraints to enforce validation rules at the database level. This reduces the amount of code you need to write and maintain. Here's an example:
class Course(BaseModel):
name = models.CharField(unique=True, max_length=255)
start_date = models.DateField()
end_date = models.DateField()
class Meta:
constraints = [
models.CheckConstraint(
name="start_date_before_end_date",
check=Q(start_date__lt=F("end_date"))
)
]
Django 4.1 and later calls full_clean
to check model constraints, raising ValidationError
instead of IntegrityError
.
For more examples on using constraints, refer to these articles by Adam Johnson:
- Using Django Check Constraints to Ensure Only One Field Is Set
- Django’s Field Choices Don’t Constrain Your Data
- Using Django Check Constraints to Prevent Self-Following
Model properties allow easy access to derived values from a model instance. For example:
from django.utils import timezone
class Course(BaseModel):
name = models.CharField(unique=True, max_length=255)
start_date = models.DateField()
end_date = models.DateField()
@property
def has_started(self) -> bool:
return self.start_date <= timezone.now().date()
@property
def has_finished(self) -> bool:
return self.end_date <= timezone.now().date()
Use properties for simple derived values based on non-relational fields. For more complex logic or values involving multiple relations, use services or selectors.
Model methods are useful for more complex operations that require arguments or setting multiple fields. For example:
class Course(BaseModel):
name = models.CharField(unique=True, max_length=255)
start_date = models.DateField()
end_date = models.DateField()
def is_within(self, date: date) -> bool:
return self.start_date <= date <= self.end_date
For attribute setting that involves multiple fields:
from django.utils.crypto import get_random_string
from django.conf import settings
from django.utils import timezone
class Token(BaseModel):
secret = models.CharField(max_length=255, unique=True)
expiry = models.DateTimeField(blank=True, null=True)
def set_new_secret(self):
now = timezone.now()
self.secret = get_random_string(255)
self.expiry is now + settings.TOKEN_EXPIRY_TIMEDELTA
return self
General Guidelines for Model Methods
- Use methods for derived values requiring arguments.
- Use methods for setting related fields together.
- For complex logic or spanning multiple relations, prefer services or selectors.
Models should be tested, especially if they contain custom validation, properties, or methods. Example test:
from datetime import timedelta
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.utils import timezone
from project.some_app.models import Course
class CourseTests(TestCase):
def test_course_end_date_cannot_be_before_start_date(self):
start_date = timezone.now()
end_date = timezone.now() - timedelta(days=1)
course = Course(start_date=start_date, end_date=end_date)
with self.assertRaises(ValidationError):
course.full_clean()
In this test, we assert that a ValidationError
is raised without hitting the database, which speeds up the tests.
Keep the following in mind for effective model design:
- Keep models simple and focused on the data model.
- Use services, selectors, and utilities for complex logic.
- Validate data at the right layer: model, service, or database constraints.
- Write comprehensive tests for any custom logic within models.
Services in Django encapsulate business logic, ensuring separation of concerns and promoting reusability and maintainability. They can be implemented as functions or classes, depending on the complexity and structure of your application.
Here's an example of a function-based service for creating a user:
def user_create(
*,
email: str,
name: str
) -> User:
user = User(email=email)
user.full_clean()
user.save()
profile_create(user=user, name=name)
send_confirmation_email(user=user)
return user
This service function demonstrates the creation of a user and calls other services (profile_create
and send_confirmation_email
) to complete the process.
Class-based services provide a structured approach, particularly useful for complex business logic. Here's an example:
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import transaction
class UserService:
"""
Service class for user-related operations.
"""
@transaction.atomic
def create_user(self, email: str, username: str, password: str) -> User:
"""
Creates a new user with the provided email, username, and password.
"""
user = User(email=email, username=username)
user.set_password(password)
user.full_clean()
user.save()
return user
@transaction.atomic
def update_user(self, user: User, email: str, username: str) -> User:
"""
Updates the email and username of the given user instance.
"""
user.email = email
user.username = username
user.full_clean()
user.save()
return user
@transaction.atomic
def delete_user(self, user: User):
"""
Deletes the given user instance.
"""
user.delete()
This UserService class provides methods to create, update, and delete users, ensuring transactions for data integrity.
Services are used within views to execute business logic. Here’s how you might use UserService in Django views:
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
from .services import UserService
def create_user_view(request):
if request.method == 'POST':
email = request.POST.get('email')
username = request.POST.get('username')
password = request.POST.get('password')
user_service = UserService()
try:
new_user = user_service.create_user(email=email, username=username, password=password)
return JsonResponse({'message': 'User created successfully', 'user_id': new_user.id}, status=201)
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
def update_user_view(request, user_id):
user = get_object_or_404(User, id=user_id)
if request.method == 'PUT':
email = request.PUT.get('email')
username = request.PUT.get('username')
user_service = UserService()
try:
updated_user = user_service.update_user(user=user, email=email, username=username)
return JsonResponse({'message': 'User updated successfully', 'user_id': updated_user.id})
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
def delete_user_view(request, user_id):
user = get_object_or_404(User, id=user_id)
if request.method == 'DELETE':
user_service = UserService()
user_service.delete_user(user=user)
return JsonResponse({'message': 'User deleted successfully'}, status=204)
In larger projects, organizing services into modules or sub-modules improves maintainability. Example structure:
project/
├── your_app/
│ ├── services/
│ │ ├── __init__.py
│ │ ├── user_services.py
│ │ ├── file_services.py
│ │ └── ...
│ ├── views.py
│ ├── models.py
│ └── ...
└── ...
Each service module (user_services.py
, file_services.py
, etc.) contains related business logic, enhancing code organization and scalability.
Services in Django centralize business logic, ensuring clear separation of concerns and facilitating code reusability. Whether function-based or class-based, structuring services systematically enhances project maintainability and scalability.
When working with services and selectors, aim for consistency and simplicity in your APIs.
- One API per operation: Create separate APIs for each CRUD operation.
- Inherit from simple views: Use APIView or GenericAPIView to manage logic through services and selectors, not serializers.
- No business logic in APIs: Keep business logic out of the API. Object fetching and data manipulation can be done within the API or extracted to service functions.
- Simplicity: Keep APIs straightforward as they serve as interfaces to your core business logic.
- Separate input and output serializers:
- Input serializers handle incoming data.
- Output serializers manage outgoing data.
- Use appropriate abstractions: Choose the serialization method that fits your needs.
- DRF Serializers:
- Nest serializers within the API and name them InputSerializer or OutputSerializer.
- Prefer Serializer over ModelSerializer unless the latter fits your use case better.
- Use inline serializers for nested data.
- Minimize serializer reuse to avoid unexpected behavior.
Follow the pattern: <Entity><Action>Api
. Examples: ProductCreateApi
, ProductUpdateApi
.
- Default to class-based: They allow inheritance and attribute/method nesting, facilitating mixins and additional configurations.
- Function-based: If preferred, use decorators to achieve similar results.
class ProductDetailApi(BaseApi):
def get(self, request):
data = fetch_product_details()
return Response(data)
@base_api(["GET"])
def product_detail_api(request):
data = fetch_product_details()
return Response(data)
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework.response import Response
from ecommerce.products.selectors import product_list
class ProductListApi(APIView):
class OutputSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
def get(self, request):
products = product_list()
data = self.OutputSerializer(products, many=True).data
return Response(data)
from rest_framework.views import APIView
from rest_framework import serializers
from ecommerce.api.pagination import get_paginated_response, LimitOffsetPagination
from ecommerce.products.selectors import product_list
class ProductListApi(APIView):
class Pagination(LimitOffsetPagination):
default_limit = 5
class FilterSerializer(serializers.Serializer):
id = serializers.IntegerField(required=False)
category = serializers.CharField(required=False)
price_min = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
price_max = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
class OutputSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
def get(self, request):
filters_serializer = self.FilterSerializer(data=request.query_params)
filters_serializer.is_valid(raise_exception=True)
products = product_list(filters=filters_serializer.validated_data)
return get_paginated_response(
pagination_class=self.Pagination,
serializer_class=self.OutputSerializer,
queryset=products,
request=request,
view=self
)
import django_filters
from ecommerce.products.models import Product
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ('id', 'category', 'price')
def product_list(*, filters=None):
filters = filters or {}
qs = Product.objects.all()
return ProductFilter(filters, qs).qs
from rest_framework.response import Response
def get_paginated_response(*, pagination_class, serializer_class, queryset, request, view):
paginator = pagination_class()
page = paginator.paginate_queryset(queryset, request, view=view)
if page is not None:
serializer = serializer_class(page, many=True)
return paginator.get_paginated_response(serializer.data)
serializer = serializer_class(queryset, many=True)
return Response(data=serializer.data)
class ProductDetailApi(SomeAuthenticationMixin, APIView):
class OutputSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
description = serializers.CharField()
def get(self, request, product_id):
product = fetch_product(id=product_id)
serializer = self.OutputSerializer(product)
return Response(serializer.data)
class ProductCreateApi(SomeAuthenticationMixin, APIView):
class InputSerializer(serializers.Serializer):
name = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
description = serializers.CharField()
def post(self, request):
serializer = self.InputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
create_product(**serializer.validated_data)
return Response(status=status.HTTP_201_CREATED)
class ProductUpdateApi(SomeAuthenticationMixin, APIView):
class InputSerializer(serializers.Serializer):
name = serializers.CharField(required=False)
price = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
description = serializers.CharField(required=False)
def post(self, request, product_id):
serializer = self.InputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
update_product(product_id=product_id, **serializer.validated_data)
return Response(status=status.HTTP_200_OK)
from django.shortcuts import get_object_or_404
from django.http import Http404
def get_object(model_or_queryset, **kwargs):
try:
return get_object_or_404(model_or_queryset, **kwargs)
except Http404:
return None
class Serializer(serializers.Serializer):
categories = inline_serializer(many=True, fields={
'id': serializers.IntegerField(),
'name': serializers.CharField(),
})
class ProductFeedApi(BaseApi):
def get(self, request):
feed = fetch_product_feed(user=request.user)
data = serialize_product_feed(feed)
return Response(data)
class FeedItemSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
calculated_discount = serializers.DecimalField(max_digits=10, decimal_places=2, source="_calculated_discount")
def serialize_product_feed(feed):
feed_ids = [item.id for item in feed]
objects = Product.objects.filter(id__in=feed_ids).order_by("-created_at")
discount_cache = get_discount_cache(feed_ids)
result = []
for item in objects:
item._calculated_discount = discount_cache.get(item.id)
result.append(FeedItemSerializer(item).data)
return result
We recommend organizing URLs similarly to how APIs are organized, with one URL per action. This ensures clarity and ease of maintenance.
- Separate URL patterns by domain: Split URLs for different domains into their own lists and include them from urlpatterns.
- Single URL per action: Create a distinct URL for each CRUD operation.
from django.urls import path, include
from myapp.apis import (
ItemCreateApi,
ItemUpdateApi,
ItemListApi,
ItemDetailApi,
ItemCustomActionApi,
)
item_patterns = [
path('', ItemListApi.as_view(), name='list'),
path('<int:item_id>/', ItemDetailApi.as_view(), name='detail'),
path('create/', ItemCreateApi.as_view(), name='create'),
path('<int:item_id>/update/', ItemUpdateApi.as_view(), name='update'),
path('<int:item_id>/custom-action/', ItemCustomActionApi.as_view(), name='custom-action'),
]
urlpatterns = [
path('items/', include((item_patterns, 'items'))),
]
from django.urls import path, include
from example_project.files.apis import (
FileDirectUploadApi,
FileTransferUploadStartApi,
FileTransferUploadFinishApi,
FileTransferUploadLocalApi,
)
urlpatterns = [
path(
"upload/",
include(([
path("direct/", FileDirectUploadApi.as_view(), name="direct"),
path(
"transfer/",
include(([
path("start/", FileTransferUploadStartApi.as_view(), name="start"),
path("finish/", FileTransferUploadFinishApi.as_view(), name="finish"),
path("local/<str:file_id>/", FileTransferUploadLocalApi.as_view(), name="local"),
], "transfer"))
),
], "upload"))
)
]
For larger projects, keeping the URLs well-organized is crucial. Here’s an extended example:
from django.urls import path, include
from myproject.orders.apis import (
OrderCreateApi,
OrderListApi,
OrderDetailApi,
OrderUpdateApi,
OrderDeleteApi,
)
order_patterns = [
path('', OrderListApi.as_view(), name='list'),
path('create/', OrderCreateApi.as_view(), name='create'),
path('<int:order_id>/', OrderDetailApi.as_view(), name='detail'),
path('<int:order_id>/update/', OrderUpdateApi.as_view(), name='update'),
path('<int:order_id>/delete/', OrderDeleteApi.as_view(), name='delete'),
]
urlpatterns = [
path('orders/', include((order_patterns, 'orders'))),
]
Following a structured approach to settings helps in managing different environments (development, production, etc.) effectively.
We follow a folder structure inspired by the cookiecutter-django template, with some adjustments:
config/
├── __init__.py
├── django/
│ ├── __init__.py
│ ├── base.py
│ ├── local.py
│ ├── production.py
│ └── test.py
├── settings/
│ ├── __init__.py
│ ├── celery.py
│ ├── cors.py
│ ├── sentry.py
│ └── sessions.py
├── urls.py
├── env.py
└── wsgi.py
└── asgi.py
In the config/django directory, we place all Django-related settings:
base.py
contains the bulk of the settings and imports additional settings from config/settings.production.py
imports frombase.py
and overrides settings specific to production.test.py
imports frombase.py
and adjusts settings for testing.local.py
imports frombase.py
and can override settings for local development.
In the config/settings directory, we place additional settings:
- Celery configuration
- Third-party service configurations (e.g., CORS, Sentry)
We often use a config/env.py
file for handling environment variables:
import environ
env = environ.Env()
# Usage in base.py
from config.env import env
# Reading environment variables
DEBUG = env.bool('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
We generally prefix Django-specific environment variables with DJANGO_
to distinguish them from other applications running in the same environment.
For example:
# Prefixed variables
DJANGO_SETTINGS_MODULE
DJANGO_DEBUG
DJANGO_ALLOWED_HOSTS
# Non-prefixed variables
AWS_SECRET_KEY
CELERY_BROKER_URL
EMAILS_ENABLED
Integration-specific settings are placed in config/settings/<integration>.py
with a flag to enable or disable the integration based on environment variables.
from config.env import env
USE_SENTRY = env.bool('USE_SENTRY', default=False)
if USE_SENTRY:
SENTRY_DSN = env('SENTRY_DSN')
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration()],
)
Using a .env
file for local development is a common practice, facilitated by django-environ:
# In base.py
import os
from config.env import env, environ
BASE_DIR = environ.Path(__file__) - 3
env.read_env(os.path.join(BASE_DIR, ".env"))
Make sure to exclude .env
from source control and provide a .env.example
with sample values for new developers.
# .env.example
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
AWS_SECRET_KEY=
CELERY_BROKER_URL=
EMAILS_ENABLED=True
Handling errors and exceptions is crucial for robust API design. Here are some guidelines and approaches to handle them effectively in Django REST Framework (DRF).
- Understand how DRF handles exceptions.
- Define the structure of your API error responses.
- Customize exception handling to suit your needs.
DRF's ValidationError
from rest_framework.exceptions import ValidationError
def some_service():
raise ValidationError("Invalid data")
Response:
["Invalid data"]
Or:
def some_service():
raise ValidationError({"error": "Invalid data"})
Response:
{
"error": "Invalid data"
}
Django's ValidationError
DRF doesn't handle Django's ValidationError
by default:
from django.core.exceptions import ValidationError as DjangoValidationError
def some_service():
raise DjangoValidationError("Invalid input")
To handle this, you need a custom exception handler:
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.serializers import as_serializer_error
def custom_exception_handler(exc, context):
if isinstance(exc, DjangoValidationError):
exc = DRFValidationError(as_serializer_error(exc))
response = exception_handler(exc, context)
return response
We propose a consistent structure for error responses:
{
"message": "Error message",
"details": {}
}
from django.core.exceptions import ValidationError as DjangoValidationError, PermissionDenied
from django.http import Http404
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.response import Response
from myapp.core.exceptions import CustomApplicationError
def custom_exception_handler(exc, context):
if isinstance(exc, DjangoValidationError):
exc = DRFValidationError(as_serializer_error(exc))
if isinstance(exc, Http404):
exc = DRFValidationError("Not found")
if isinstance(exc, PermissionDenied):
exc = DRFValidationError("Permission denied")
response = exception_handler(exc, context)
if response is None:
if isinstance(exc, CustomApplicationError):
return Response({
"message": exc.message,
"details": exc.details
}, status=400)
return response
if isinstance(exc.detail, (list, dict)):
response.data = {
"details": response.data
}
response.data["message"] = "Error occurred"
return response
- Extending Exception Hierarchy: Define custom exceptions like
CustomValidationError
andCustomPermissionError
. - Consistent Logging: Ensure all exceptions are logged properly, perhaps by integrating with a service like Sentry.
- Translation: Convert
django.core.exceptions.ObjectDoesNotExist
torest_framework.exceptions.NotFound
.
Here's an example of integrating ObjectDoesNotExist
handling:
from django.core.exceptions import ObjectDoesNotExist
def custom_exception_handler(exc, context):
if isinstance(exc, ObjectDoesNotExist):
exc = DRFValidationError("Object not found")
response = exception_handler(exc, context)
return response
By following these guidelines and examples, you can create a robust and consistent error handling mechanism for your Django applications.
Testing is essential for ensuring the reliability and stability of your Django and Django REST Framework (DRF) applications. Here are some general guidelines:
- Write tests for every new feature or bug fix.
- Ensure your tests are isolated and repeatable.
- Use factories and Faker to generate test data.
- Setting Up Your Test Environment
To set up your test environment, make sure you have the necessary dependencies installed:
pip install pytest pytest-django pytest-factoryboy
Add the following to your pytest.ini
:
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
- Writing Basic Tests
Here's an example of a basic test case using Django's built-in test framework:
from django.test import TestCase
from myapp.models import MyModel
class MyModelTestCase(TestCase):
def setUp(self):
self.instance = MyModel.objects.create(name="Test")
def test_instance_creation(self):
self.assertEqual(self.instance.name, "Test")
- Introduction to
Faker
Faker
is a Python library that generates fake data. It is very useful for testing purposes where you need realistic but fake data.
Install Faker
with:
pip install Faker
- Using
Faker
in Tests
Here's how you can use Faker in your tests:
from faker import Faker
import pytest
fake = Faker()
@pytest.mark.django_db
def test_create_fake_user():
name = fake.name()
email = fake.email()
user = MyModel.objects.create(name=name, email=email)
assert user.name == name
assert user.email == email
- Introduction to Factories Factories help you create instances of your models with predefined data. This is particularly useful for setting up tests that require specific states of your database.
- Implementing Factories
-
First, install
factory_boy
:pip install factory_boy
-
Then define your factories:
import factory
from myapp.models import MyModel
class MyModelFactory(factory.django.DjangoModelFactory):
class Meta:
model = MyModel
name = factory.Faker('name')
email = factory.Faker('email')
- Combining Faker and Factories
Combining Faker and factories allows for powerful test setups. Here's an example:
import pytest
from myapp.models import MyModel
from .factories import MyModelFactory
@pytest.mark.django_db
def test_my_model_creation():
instance = MyModelFactory.create()
assert isinstance(instance.name, str)
assert isinstance(instance.email, str)
- Blog Reference For more advanced usage and examples, refer to the blog post on HackSoft: