Skip to content

Testing

Guide to running and writing tests for AppArt Agent.

Overview

Test Type Framework Location
Backend Unit pytest backend/tests/
Backend Integration pytest backend/tests/
Frontend Unit Jest frontend/__tests__/
E2E Playwright e2e/ (planned)

Running Tests

Backend Tests

# Docker
docker-compose exec backend pytest

# Local
cd backend
source .venv/bin/activate
pytest

# With coverage
pytest --cov=app --cov-report=html

# Specific test file
pytest tests/test_dvf_service.py -v

# Specific test function
pytest tests/test_dvf_service.py::test_address_parsing -v

# Run only failed tests
pytest --lf

Frontend Tests

# Docker
docker-compose exec frontend pnpm test

# Local
cd frontend
pnpm test

# Watch mode
pnpm test:watch

# With coverage
pnpm test:coverage

Backend Testing

Test Structure

backend/tests/
├── __init__.py
├── conftest.py          # Shared fixtures
├── test_dvf_service.py  # DVF service tests
├── test_api/            # API endpoint tests
│   ├── test_analysis.py
│   ├── test_documents.py
│   └── test_properties.py
└── test_services/       # Service unit tests
    ├── test_ai_services.py
    └── test_storage.py

Writing Tests

Basic Test

# tests/test_dvf_service.py
import pytest
from app.services.dvf_service import DVFService

def test_address_parsing():
    """Test address normalization."""
    result = DVFService.normalize_address("56 rue notre-dame des champs")
    assert result == "56 RUE NOTRE-DAME DES CHAMPS"

Async Test

import pytest
from app.services.ai.document_analyzer import DocumentAnalyzer

@pytest.mark.asyncio
async def test_document_classification():
    """Test document type classification."""
    analyzer = DocumentAnalyzer()
    result = await analyzer.classify_document(
        images=[test_image],
        filename="pv_ag_2024.pdf"
    )
    assert result["document_type"] in ["pv_ag", "diagnostic", "tax", "charges"]

Database Test

import pytest
from sqlalchemy.orm import Session
from app.models.property import Property

def test_create_property(db_session: Session):
    """Test property creation."""
    property = Property(
        address="56 Rue Notre-Dame des Champs",
        postal_code="75006",
        city="Paris",
        asking_price=850000
    )
    db_session.add(property)
    db_session.commit()

    assert property.id is not None
    assert property.price_per_sqm is None  # Surface not set

Fixtures

# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.database import Base

@pytest.fixture
def db_session():
    """Create a test database session."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    session.close()

@pytest.fixture
def test_user(db_session):
    """Create a test user."""
    from app.models.user import User
    user = User(
        email="test@example.com",
        hashed_password="hashed",
        full_name="Test User"
    )
    db_session.add(user)
    db_session.commit()
    return user

Mocking

from unittest.mock import AsyncMock, patch

@pytest.mark.asyncio
async def test_document_analysis_with_mock():
    """Test with mocked AI service."""
    with patch('app.services.ai.document_analyzer.DocumentAnalyzer') as MockAnalyzer:
        mock_instance = MockAnalyzer.return_value
        mock_instance.analyze_document = AsyncMock(return_value={
            "summary": "Test summary",
            "key_findings": ["Finding 1"]
        })

        # Test your code that uses DocumentAnalyzer
        result = await process_document(test_doc)

        assert result["summary"] == "Test summary"

Frontend Testing

Test Structure

frontend/
├── __tests__/
│   ├── components/
│   │   └── Header.test.tsx
│   └── lib/
│       └── api.test.ts
└── src/
    └── components/
        └── Header.tsx

Writing Tests

Component Test

// __tests__/components/Header.test.tsx
import { render, screen } from '@testing-library/react';
import { Header } from '@/components/Header';

describe('Header', () => {
  it('renders logo', () => {
    render(<Header />);
    expect(screen.getByText('AppArt Agent')).toBeInTheDocument();
  });

  it('shows login when not authenticated', () => {
    render(<Header />);
    expect(screen.getByText('Login')).toBeInTheDocument();
  });
});

Hook Test

// __tests__/hooks/useAuth.test.tsx
import { renderHook, act } from '@testing-library/react';
import { AuthProvider, useAuth } from '@/contexts/AuthContext';

describe('useAuth', () => {
  it('provides authentication state', () => {
    const wrapper = ({ children }) => (
      <AuthProvider>{children}</AuthProvider>
    );

    const { result } = renderHook(() => useAuth(), { wrapper });

    expect(result.current.user).toBeNull();
    expect(result.current.loading).toBe(false);
  });
});

API Test

// __tests__/lib/api.test.ts
import { api } from '@/lib/api';

describe('api', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  it('makes GET request with auth header', async () => {
    (fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ data: 'test' })
    });

    await api.get('/properties');

    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/properties'),
      expect.objectContaining({
        method: 'GET',
        headers: expect.objectContaining({
          'Authorization': expect.any(String)
        })
      })
    );
  });
});

Test Coverage

Backend Coverage Report

pytest --cov=app --cov-report=html
open htmlcov/index.html

Frontend Coverage Report

pnpm test:coverage
open coverage/lcov-report/index.html

Coverage Targets

Area Target Current
Backend overall 80% TBD
API endpoints 90% TBD
AI services 70% TBD
Frontend components 80% TBD

Load Testing with Locust

Load tests live at loadtest/locustfile.py in the project root. Locust is installed as a root-level dev dependency.

Running Load Tests

# Backend API load test (Web UI at http://localhost:8089)
uv run python -m locust -f loadtest/locustfile.py --host https://api.appartagent.com

# Frontend SSR pages (run separately with different host)
uv run python -m locust -f loadtest/locustfile.py --host https://appartagent.com FrontendUser

# Headless mode for CI
uv run python -m locust -f loadtest/locustfile.py --host https://api.appartagent.com --headless -u 50 -r 5 --run-time 2m AppArtUser

User Classes

Class Target Description
AppArtUser Backend API Exercises REST endpoints with auth
FrontendUser Frontend SSR Loads rendered pages

Environment Variables

Variable Description
LOCUST_AUTH_TOKEN Better Auth session cookie for authenticated requests
LOCUST_PROPERTY_ID Property ID to use in tests (default: 1)

Baseline Results (50 users)

Metric Value
P50 220ms
P90 8.7s
P95 15s
P99 25s
Error rate 0%

Slowest endpoint before caching: /api/properties/dvf-stats (median 1.6s). Even /health had P90 of 5.5s due to request queuing at high concurrency.

CI/CD Integration

Tests run automatically on pull requests:

# .github/workflows/test.yml
name: Tests
on: [pull_request]

jobs:
  backend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: |
          cd backend
          uv pip install -e ".[dev]"
          pytest --cov=app

  frontend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - run: |
          cd frontend
          pnpm install
          pnpm test