Skip to content

DjangoPerformanceAPITestCase

smattymatty edited this page Aug 27, 2025 · 1 revision

DjangoPerformanceAPITestCase

The production test case for documenting and enforcing performance requirements in Django applications. This class provides manual control with specific assertions for CI/CD pipelines.

Overview

DjangoPerformanceAPITestCase is designed for the documentation phase of performance testing. It provides explicit performance assertions that document requirements and fail tests when standards aren't met.

Key Features

  • ✅ Explicit performance assertions for CI/CD
  • 📊 Manual monitoring control
  • 🎯 Specific threshold enforcement
  • 📈 Performance tracking and reporting
  • 🔧 Context managers for monitoring
  • 📝 Production-ready test documentation

Quick Start

from django_mercury import DjangoPerformanceAPITestCase
from django_mercury import monitor_django_view

class MyPerformanceTests(DjangoPerformanceAPITestCase):
    def test_user_list_performance(self):
        with monitor_django_view("UserList") as monitor:
            response = self.client.get('/api/users/')
        
        # Explicit assertions document requirements
        self.assertResponseTimeLess(monitor, 100)  # Must respond in 100ms
        self.assertQueriesLess(monitor, 5)          # Maximum 5 queries
        self.assertNoN1Queries(monitor)             # No N+1 queries allowed

Methods

Core Assertion Methods

assertPerformance()

Comprehensive performance assertion with multiple criteria.

def assertPerformance(
    self,
    monitor: EnhancedPerformanceMonitor,
    max_response_time: Optional[float] = None,
    max_memory_mb: Optional[float] = None,
    max_queries: Optional[int] = None,
    min_cache_hit_ratio: Optional[float] = None,
    msg: Optional[str] = None
) -> None

Example:

def test_comprehensive_performance(self):
    with monitor_django_view("ComplexOperation") as monitor:
        response = self.client.post('/api/complex/')
    
    self.assertPerformance(
        monitor,
        max_response_time=200,      # 200ms max
        max_memory_mb=50,           # 50MB max memory
        max_queries=10,             # 10 queries max
        min_cache_hit_ratio=0.8,    # 80% cache hits minimum
        msg="Complex operation performance requirements"
    )

assertResponseTimeLess()

Assert response time is under threshold.

def assertResponseTimeLess(
    self,
    metrics_or_monitor: Union[EnhancedPerformanceMonitor, EnhancedPerformanceMetrics_Python],
    milliseconds: float,
    msg: Optional[str] = None
) -> None

Example:

def test_fast_endpoint(self):
    metrics = self.run_comprehensive_analysis(
        "FastEndpoint",
        lambda: self.client.get('/api/fast/')
    )
    self.assertResponseTimeLess(metrics, 50, "Should be very fast")

assertMemoryLess()

Assert memory usage is under threshold.

def assertMemoryLess(
    self,
    metrics_or_monitor: Union[EnhancedPerformanceMonitor, EnhancedPerformanceMetrics_Python],
    megabytes: float,
    msg: Optional[str] = None
) -> None

Example:

def test_memory_efficient(self):
    with monitor_django_view("DataProcessing") as monitor:
        response = self.client.post('/api/process/')
    
    self.assertMemoryLess(monitor, 100, "Should not exceed 100MB")

assertQueriesLess()

Assert database query count is under threshold.

def assertQueriesLess(
    self,
    metrics_or_monitor: Union[EnhancedPerformanceMonitor, EnhancedPerformanceMetrics_Python],
    count: int,
    msg: Optional[str] = None
) -> None

Example:

def test_optimized_queries(self):
    with monitor_django_view("UserDetail") as monitor:
        response = self.client.get('/api/users/1/')
    
    self.assertQueriesLess(monitor, 3, "Should use select_related")

assertCacheHitRatioGreater()

Assert cache hit ratio meets minimum.

def assertCacheHitRatioGreater(
    self,
    metrics_or_monitor: Union[EnhancedPerformanceMonitor, EnhancedPerformanceMetrics_Python],
    ratio: float,
    msg: Optional[str] = None
) -> None

Example:

def test_cache_effectiveness(self):
    with monitor_django_view("CachedView") as monitor:
        for _ in range(10):
            self.client.get('/api/cached/')
    
    self.assertCacheHitRatioGreater(monitor, 0.9, "90% cache hit required")

assertNoN1Queries()

Assert no N+1 query patterns exist.

def assertNoN1Queries(
    self,
    metrics_or_monitor: Union[EnhancedPerformanceMonitor, EnhancedPerformanceMetrics_Python],
    msg: Optional[str] = None
) -> None

Example:

def test_no_n_plus_one(self):
    with monitor_django_view("UserListWithProfiles") as monitor:
        response = self.client.get('/api/users/?include=profile')
    
    self.assertNoN1Queries(monitor, "Must use select_related")

Monitoring Methods

monitor_django_view()

Context manager for monitoring Django views.

def monitor_django_view(
    self,
    operation_name: str,
    auto_report: bool = False
) -> EnhancedPerformanceMonitor

Example:

def test_view_performance(self):
    monitor = self.monitor_django_view("CreateUser", auto_report=True)
    with monitor:
        response = self.client.post('/api/users/', data={...})
    
    # Auto-report prints performance summary
    self.assertResponseTimeLess(monitor, 200)

monitor_serializer()

Context manager for monitoring serializer performance.

def monitor_serializer(
    self,
    serializer_name: str,
    auto_report: bool = False
) -> EnhancedPerformanceMonitor

Example:

def test_serializer_performance(self):
    from myapp.serializers import UserSerializer
    
    monitor = self.monitor_serializer("UserSerializer")
    with monitor:
        serializer = UserSerializer(users, many=True)
        data = serializer.data
    
    self.assertResponseTimeLess(monitor, 50)

monitor_django_model()

Context manager for monitoring model operations.

def monitor_django_model(
    self,
    model_name: str,
    auto_report: bool = False
) -> EnhancedPerformanceMonitor

Example:

def test_model_performance(self):
    monitor = self.monitor_django_model("User.bulk_create")
    with monitor:
        User.objects.bulk_create([
            User(username=f"user_{i}") for i in range(1000)
        ])
    
    self.assertResponseTimeLess(monitor, 1000)
    self.assertQueriesLess(monitor, 2)

Analysis Methods

run_comprehensive_analysis()

Run detailed performance analysis on a test function.

def run_comprehensive_analysis(
    self,
    operation_name: str,
    test_function: callable,
    operation_type: str = "general",
    expect_response_under: Optional[float] = None,
    expect_memory_under: Optional[float] = None,
    expect_queries_under: Optional[int] = None,
    print_analysis: bool = True,
    show_scoring: bool = True,
    **kwargs
) -> Optional[EnhancedPerformanceMetrics_Python]

Example:

def test_comprehensive_analysis(self):
    metrics = self.run_comprehensive_analysis(
        "ComplexSearch",
        lambda: self.client.get('/api/search?q=test'),
        operation_type="search_view",
        expect_response_under=300,
        expect_queries_under=10,
        print_analysis=True,      # Show detailed analysis
        show_scoring=True,         # Show performance grade
        auto_detect_n_plus_one=True
    )
    
    # Metrics contains detailed performance data
    self.assertGreater(metrics.performance_score.total_score, 60)

Reporting Methods

print_performance_dashboard()

Display a formatted performance dashboard.

def print_performance_dashboard(
    self,
    metrics: EnhancedPerformanceMetrics_Python,
    title: str = "Performance Dashboard"
) -> None

Example:

def test_with_dashboard(self):
    metrics = self.run_comprehensive_analysis(
        "Dashboard",
        lambda: self.client.get('/api/dashboard/')
    )
    
    self.print_performance_dashboard(
        metrics,
        title="Dashboard API Performance"
    )

Context Managers

Using monitor_django_view

# Basic usage
with monitor_django_view("OperationName") as monitor:
    # Your code here
    response = self.client.get('/api/endpoint/')

# Access metrics after context
print(f"Response time: {monitor.metrics.response_time}ms")
print(f"Query count: {monitor.metrics.query_count}")

Nested Monitoring

def test_nested_operations(self):
    with monitor_django_view("ParentOperation") as parent:
        response = self.client.get('/api/parent/')
        
        with monitor_django_view("ChildOperation") as child:
            self.client.get('/api/child/')
        
        self.assertResponseTimeLess(child, 50)
    
    self.assertResponseTimeLess(parent, 150)

Operation Types

Specify operation types for better analysis:

def test_with_operation_type(self):
    metrics = self.run_comprehensive_analysis(
        "UserList",
        lambda: self.client.get('/api/users/'),
        operation_type="list_view"  # Adjusts expectations
    )

Available Operation Types

  • general - Default, balanced thresholds
  • list_view - Multiple items, allows more queries
  • detail_view - Single item, expects fewer queries
  • create_view - Creation, allows validation queries
  • update_view - Updates, moderate query count
  • delete_view - Deletion, allows cascade queries
  • search_view - Search/filter, allows complex queries

Production Testing Patterns

Pattern 1: Simple Assertions

def test_simple_performance(self):
    with monitor_django_view("Simple") as monitor:
        response = self.client.get('/api/simple/')
    
    self.assertResponseTimeLess(monitor, 100)
    self.assertQueriesLess(monitor, 5)

Pattern 2: Comprehensive Testing

def test_comprehensive(self):
    metrics = self.run_comprehensive_analysis(
        "Comprehensive",
        lambda: self.client.post('/api/complex/', data={}),
        expect_response_under=200,
        expect_memory_under=100,
        expect_queries_under=20
    )
    
    # Additional assertions
    self.assertNoN1Queries(metrics)
    self.assertGreater(metrics.performance_score.total_score, 70)

Pattern 3: Batch Testing

def test_batch_operations(self):
    operations = [
        ("List", lambda: self.client.get('/api/items/'), 100, 10),
        ("Create", lambda: self.client.post('/api/items/'), 200, 15),
        ("Delete", lambda: self.client.delete('/api/items/1/'), 150, 20),
    ]
    
    for name, operation, max_time, max_queries in operations:
        with self.subTest(operation=name):
            with monitor_django_view(name) as monitor:
                operation()
            
            self.assertResponseTimeLess(monitor, max_time)
            self.assertQueriesLess(monitor, max_queries)

CI/CD Integration

Example: GitHub Actions

- name: Run Performance Tests
  run: |
    python manage.py test tests.performance \
      --parallel \
      --verbosity=2
  env:
    DJANGO_MERCURY_ENABLED: "true"

Example: pytest

import pytest
from django_mercury import DjangoPerformanceAPITestCase

@pytest.mark.performance
class TestAPIPerformance(DjangoPerformanceAPITestCase):
    def test_critical_endpoint(self):
        with monitor_django_view("Critical") as monitor:
            response = self.client.get('/api/critical/')
        
        self.assertResponseTimeLess(monitor, 50)
        self.assertQueriesLess(monitor, 3)

Best Practices

1. Document Requirements

def test_sla_requirements(self):
    """API must respond within 100ms per SLA agreement."""
    with monitor_django_view("SLA") as monitor:
        response = self.client.get('/api/sla-endpoint/')
    
    self.assertResponseTimeLess(monitor, 100, "SLA requirement")

2. Use Descriptive Names

with monitor_django_view("UserListWithFullProfiles") as monitor:
    # Clear operation name helps debugging

3. Set Realistic Thresholds

# Consider actual requirements
self.assertResponseTimeLess(monitor, 200)  # User-facing
self.assertResponseTimeLess(monitor, 5000) # Background task

4. Test Under Load

def test_concurrent_requests(self):
    """Test performance under concurrent load."""
    from concurrent.futures import ThreadPoolExecutor
    
    def make_request():
        return self.client.get('/api/concurrent/')
    
    with monitor_django_view("ConcurrentLoad") as monitor:
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [executor.submit(make_request) for _ in range(10)]
    
    self.assertResponseTimeLess(monitor, 1000)

Migration from Mercury to Performance

Before (Investigation with Mercury)

class InvestigationTests(DjangoMercuryAPITestCase):
    def test_discover_issues(self):
        response = self.client.get('/api/users/')
        # Mercury discovers issues automatically

After (Documentation with Performance)

class ProductionTests(DjangoPerformanceAPITestCase):
    def test_enforce_standards(self):
        with monitor_django_view("UserList") as monitor:
            response = self.client.get('/api/users/')
        
        # Explicit assertions based on discovered requirements
        self.assertResponseTimeLess(monitor, 100)
        self.assertQueriesLess(monitor, 5)
        self.assertNoN1Queries(monitor)

Error Messages

Performance assertions provide clear error messages:

AssertionError: Response time 150.5ms is not less than 100ms

AssertionError: Query count 25 exceeds maximum 10

AssertionError: N+1 query pattern detected: 50 similar queries found
  Fix: Use select_related('profile') or prefetch_related('tags')

AssertionError: Cache hit ratio 0.45 is not greater than 0.8

See Also

Django Mercury Wiki

🏠 Overview

Clone this wiki locally