Testing Guide¶
Comprehensive testing strategy and guidelines for Juddges Legal Assistant.
Table of Contents¶
- Testing Philosophy
- Testing Stack
- Backend Testing
- Frontend Testing
- Integration Testing
- E2E Testing
- Test Coverage
- Best Practices
- CI/CD Testing
Testing Philosophy¶
Why We Test¶
- Catch bugs early: Find issues before they reach production
- Enable refactoring: Change code with confidence
- Document behavior: Tests serve as living documentation
- Improve design: Writing tests leads to better code structure
- Prevent regressions: Ensure fixes stay fixed
Testing Pyramid¶
/\
/ \ E2E Tests (Few, Slow, High-level)
/____\
/ \ Integration Tests (Some, Medium, Multi-component)
/ \
/__________\ Unit Tests (Many, Fast, Single-component)
Target Distribution: - 70% Unit Tests: Fast, isolated, test individual functions - 20% Integration Tests: Test component interactions - 10% E2E Tests: Test complete user flows
Testing Stack¶
Backend¶
- Framework: pytest
- Markers:
@pytest.mark.unit,@pytest.mark.integration - Coverage: pytest-cov
- Async Testing: pytest-asyncio
- Fixtures: pytest fixtures for setup/teardown
- Mocking: unittest.mock, pytest-mock
Frontend¶
- Unit Testing: Jest + Testing Library
- E2E Testing: Playwright
- Component Testing: React Testing Library
- Coverage: Jest coverage reports
- Mocking: Jest mocks, MSW for API mocking
Backend Testing¶
Setup¶
cd backend
# Install test dependencies
poetry install
# Run all tests
poetry run pytest tests/ -v
# Run with coverage
poetry run pytest tests/ --cov=app --cov-report=html
Test Structure¶
backend/tests/
├── conftest.py # Shared fixtures
├── app/
│ ├── test_documents.py # Document endpoint tests
│ ├── test_search.py # Search endpoint tests
│ └── test_analytics.py # Analytics endpoint tests
└── packages/
└── juddges_search/
├── test_chains.py # LangChain tests
└── test_db.py # Database tests
Unit Tests¶
Test individual functions in isolation.
# tests/app/test_utils.py
import pytest
from app.utils import validate_id_format, parse_date
@pytest.mark.unit
def test_validate_id_format_valid():
"""Test ID validation with valid UUID."""
valid_id = "550e8400-e29b-41d4-a716-446655440000"
assert validate_id_format(valid_id) is True
@pytest.mark.unit
def test_validate_id_format_invalid():
"""Test ID validation with invalid UUID."""
invalid_id = "not-a-uuid"
assert validate_id_format(invalid_id) is False
@pytest.mark.unit
@pytest.mark.parametrize("date_str,expected", [
("2024-01-15", "2024-01-15"),
("2024-01-15T10:30:00Z", "2024-01-15"),
("invalid", None),
])
def test_parse_date(date_str, expected):
"""Test date parsing with various formats."""
result = parse_date(date_str)
if expected:
assert result.strftime("%Y-%m-%d") == expected
else:
assert result is None
Integration Tests¶
Test component interactions with real dependencies.
# tests/app/test_documents.py
import pytest
from fastapi.testclient import TestClient
from app.server import app
@pytest.fixture
def client():
"""FastAPI test client."""
return TestClient(app)
@pytest.mark.integration
def test_search_documents(client):
"""Test document search endpoint."""
response = client.post(
"/api/v1/documents/search",
json={
"query": "contract law",
"filters": {"jurisdiction": "PL"},
"limit": 10
},
headers={"X-API-Key": "test-api-key"}
)
assert response.status_code == 200
data = response.json()
assert "results" in data
assert isinstance(data["results"], list)
assert len(data["results"]) <= 10
assert "total" in data
assert all(r["jurisdiction"] == "PL" for r in data["results"])
@pytest.mark.integration
async def test_vector_search():
"""Test vector database search."""
from juddges_search.db.supabase_db import get_vector_db
db = get_vector_db()
results = await db.similarity_search(
query="contract interpretation",
k=5
)
assert len(results) <= 5
assert all(hasattr(r, "document_id") for r in results)
assert all(hasattr(r, "score") for r in results)
Fixtures¶
Reusable test setup.
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.server import app
@pytest.fixture
def client():
"""FastAPI test client."""
return TestClient(app)
@pytest.fixture
def api_key():
"""Test API key."""
return "test-api-key"
@pytest.fixture
def sample_document():
"""Sample document for testing."""
return {
"id": "550e8400-e29b-41d4-a716-446655440000",
"case_number": "I CSK 123/2023",
"title": "Test Case",
"jurisdiction": "PL",
"full_text": "Test judgment text"
}
@pytest.fixture
async def db_connection():
"""Database connection for testing."""
from juddges_search.db.supabase_db import get_vector_db
db = get_vector_db()
yield db
# Cleanup if needed
Mocking¶
Mock external dependencies.
import pytest
from unittest.mock import AsyncMock, patch
@pytest.mark.unit
@patch("app.documents.generate_embedding")
async def test_search_with_mocked_embeddings(mock_generate):
"""Test search with mocked embedding generation."""
# Mock the embedding generation
mock_generate.return_value = [0.1] * 1536
from app.documents import search_documents
results = await search_documents("test query")
# Verify embedding was generated
mock_generate.assert_called_once_with("test query")
assert isinstance(results, list)
Async Testing¶
Test async functions.
import pytest
@pytest.mark.asyncio
async def test_async_document_fetch():
"""Test async document fetching."""
from app.documents import get_document_by_id
doc_id = "550e8400-e29b-41d4-a716-446655440000"
document = await get_document_by_id(doc_id)
assert document is not None
assert document.id == doc_id
Running Backend Tests¶
# All tests
poetry run pytest tests/ -v
# Unit tests only (fast)
poetry run pytest tests/ -v -m unit
# Integration tests only (slower)
poetry run pytest tests/ -v -m integration
# Specific test file
poetry run pytest tests/app/test_documents.py -v
# Specific test function
poetry run pytest tests/app/test_documents.py::test_search_documents -v
# With coverage
poetry run pytest tests/ --cov=app --cov-report=html
open htmlcov/index.html
# Using Poe tasks
poetry run poe test
poetry run poe test-unit
poetry run poe test-integration
poetry run poe test-cov
Frontend Testing¶
Setup¶
cd frontend
# Install dependencies
npm install
# Run tests
npm test
# Run with coverage
npm run test:coverage
# Watch mode
npm run test:watch
Test Structure¶
frontend/__tests__/
├── components/
│ ├── ui/
│ │ ├── Button.test.tsx
│ │ └── Input.test.tsx
│ ├── search/
│ │ ├── SearchBar.test.tsx
│ │ └── SearchResults.test.tsx
│ └── chat/
│ └── ChatInterface.test.tsx
├── lib/
│ ├── utils.test.ts
│ └── api/
│ └── documents.test.ts
└── hooks/
└── useSearch.test.ts
Component Tests¶
Test React components.
// __tests__/components/search/SearchBar.test.tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SearchBar } from "@/components/search/SearchBar";
describe("SearchBar", () => {
it("renders with placeholder", () => {
render(<SearchBar onSearch={jest.fn()} />);
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
});
it("calls onSearch when form is submitted", async () => {
const onSearch = jest.fn();
const user = userEvent.setup();
render(<SearchBar onSearch={onSearch} />);
const input = screen.getByPlaceholderText("Search...");
const button = screen.getByRole("button", { name: /search/i });
await user.type(input, "contract law");
await user.click(button);
expect(onSearch).toHaveBeenCalledWith("contract law");
});
it("does not call onSearch with empty query", async () => {
const onSearch = jest.fn();
const user = userEvent.setup();
render(<SearchBar onSearch={onSearch} />);
const button = screen.getByRole("button", { name: /search/i });
await user.click(button);
expect(onSearch).not.toHaveBeenCalled();
});
});
Hook Tests¶
Test custom React hooks.
// __tests__/hooks/useSearch.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useSearch } from "@/hooks/useSearch";
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe("useSearch", () => {
it("returns search results", async () => {
const { result } = renderHook(
() => useSearch({ query: "contract law" }),
{ wrapper: createWrapper() }
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toBeDefined();
expect(result.current.data?.results).toBeInstanceOf(Array);
});
it("handles empty query", async () => {
const { result } = renderHook(
() => useSearch({ query: "" }),
{ wrapper: createWrapper() }
);
expect(result.current.data).toBeUndefined();
expect(result.current.isLoading).toBe(false);
});
});
Utility Tests¶
Test utility functions.
// __tests__/lib/utils.test.ts
import { cn, formatDate, truncateText } from "@/lib/utils";
describe("cn", () => {
it("merges class names", () => {
expect(cn("class1", "class2")).toBe("class1 class2");
});
it("handles conditional classes", () => {
const isActive = true;
expect(cn("base", isActive && "active")).toBe("base active");
});
});
describe("formatDate", () => {
it("formats ISO date", () => {
const date = "2024-02-13T10:30:00Z";
expect(formatDate(date)).toBe("Feb 13, 2024");
});
it("handles invalid date", () => {
expect(formatDate("invalid")).toBe("Invalid date");
});
});
describe("truncateText", () => {
it("truncates long text", () => {
const text = "This is a very long text that should be truncated";
expect(truncateText(text, 20)).toBe("This is a very lo...");
});
it("does not truncate short text", () => {
const text = "Short text";
expect(truncateText(text, 20)).toBe("Short text");
});
});
API Mocking¶
Mock API calls with MSW (Mock Service Worker).
// __tests__/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.post("/api/v1/documents/search", () => {
return HttpResponse.json({
results: [
{
id: "1",
title: "Test Document",
case_number: "I CSK 123/2023",
},
],
total: 1,
});
}),
http.get("/api/v1/documents/:id", ({ params }) => {
return HttpResponse.json({
id: params.id,
title: "Test Document",
full_text: "Document content",
});
}),
];
// __tests__/setup.ts
import { setupServer } from "msw/node";
import { handlers } from "./mocks/handlers";
export const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Running Frontend Tests¶
# All tests
npm test
# Watch mode (re-runs on file changes)
npm run test:watch
# Coverage report
npm run test:coverage
# Specific test file
npm test -- SearchBar.test.tsx
# Update snapshots
npm test -- -u
# Verbose output
npm test -- --verbose
E2E Testing¶
Setup¶
cd frontend
# Install Playwright
npx playwright install
# Run E2E tests
npm run test:e2e
# Run with UI (visual debugger)
npm run test:e2e:ui
# Run specific suite
npm run test:e2e:search
E2E Test Structure¶
frontend/tests/e2e/
├── auth/
│ └── login.spec.ts
├── search/
│ ├── basic-search.spec.ts
│ └── advanced-search.spec.ts
├── chat/
│ └── conversation.spec.ts
└── documents/
└── document-view.spec.ts
E2E Test Examples¶
// tests/e2e/search/basic-search.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Document Search", () => {
test("search for documents", async ({ page }) => {
await page.goto("http://localhost:3007/search");
// Enter search query
await page.fill('input[name="query"]', "contract law");
await page.click('button[type="submit"]');
// Wait for results
await expect(page.locator(".search-results")).toBeVisible();
// Verify results
const results = page.locator(".result-item");
await expect(results).toHaveCount(10);
// Verify result content
const firstResult = results.first();
await expect(firstResult).toContainText("contract");
});
test("filter by jurisdiction", async ({ page }) => {
await page.goto("http://localhost:3007/search");
// Enter query
await page.fill('input[name="query"]', "contract law");
// Select jurisdiction filter
await page.click('button[aria-label="Jurisdiction"]');
await page.click('text=Poland (PL)');
// Submit search
await page.click('button[type="submit"]');
// Verify filtered results
await expect(page.locator(".result-item")).toHaveCount(10);
const jurisdictions = await page.locator(".result-jurisdiction").allTextContents();
expect(jurisdictions.every(j => j === "PL")).toBe(true);
});
test("paginate through results", async ({ page }) => {
await page.goto("http://localhost:3007/search");
// Search
await page.fill('input[name="query"]', "contract");
await page.click('button[type="submit"]');
// Go to page 2
await page.click('button[aria-label="Next page"]');
// Verify pagination
await expect(page.locator('text=Page 2')).toBeVisible();
});
});
Test Coverage¶
Target Coverage¶
- Unit Tests: 80%+ coverage
- Integration Tests: 60%+ coverage
- E2E Tests: Critical paths only
Generate Coverage Reports¶
Backend:
cd backend
poetry run pytest tests/ --cov=app --cov-report=html --cov-report=term-missing
open htmlcov/index.html
Frontend:
Coverage Metrics¶
- Line Coverage: Percentage of code lines executed
- Branch Coverage: Percentage of conditional branches tested
- Function Coverage: Percentage of functions called
- Statement Coverage: Percentage of statements executed
Best Practices¶
General¶
- Test behavior, not implementation: Focus on what, not how
- Write clear test names: Test name should describe expected behavior
- One assertion per test: Or at least, one concept per test
- Arrange-Act-Assert: Structure tests clearly
- Keep tests independent: Each test should run in isolation
- Use fixtures/factories: Reuse test data setup
- Test edge cases: Empty inputs, boundaries, errors
Naming¶
# Good - descriptive test names
def test_search_returns_empty_list_when_no_results_found():
pass
def test_search_raises_error_when_query_too_short():
pass
# Bad - unclear names
def test_search():
pass
def test_error():
pass
Assertions¶
# Good - specific assertions
assert len(results) == 10
assert result.status_code == 200
assert "error" not in response.json()
# Bad - vague assertions
assert results
assert response
Test Data¶
# Good - use factories/fixtures
@pytest.fixture
def sample_documents():
return [
create_document(title="Doc 1"),
create_document(title="Doc 2"),
]
# Bad - hardcoded data in tests
def test_search():
doc1 = {"id": "1", "title": "Doc 1", ...}
doc2 = {"id": "2", "title": "Doc 2", ...}
CI/CD Testing¶
GitHub Actions¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
backend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
cd backend
pip install poetry
poetry install
- name: Run tests
run: |
cd backend
poetry run pytest tests/ -v --cov=app
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "20"
- name: Install dependencies
run: |
cd frontend
npm install
- name: Run tests
run: |
cd frontend
npm test -- --coverage
For more information: - DEVELOPER_ONBOARDING.md - Setup guide - CODE_STYLE.md - Coding standards - CONTRIBUTING.md - Contribution guidelines