Skip to content

Commit 70818c7

Browse files
committed
test: add comprehensive testing infrastructure and initial test suite
- Introduce `tests/` directory with organized structure: unit, integration, API, and security - Add shared pytest fixtures in `conftest.py` for dependency injection and test configuration - Create `docker-compose.test.yml` to run PostgreSQL test database on port 5433 - Add `TESTING.md` to document test setup, running instructions, structure, and best practices - Include unit tests for token utility functions (`test_token_utils.py`) - Add API tests for paste endpoints (`test_paste_routes.py`) - Introduce pytest configuration and markers in `pytest.ini` - Add setup script (`setup_test_db.sh`) for test database initialization - Ensure test coverage integration with pytest plugins and CI compatibility
1 parent 3bd7f0e commit 70818c7

File tree

15 files changed

+598
-0
lines changed

15 files changed

+598
-0
lines changed

backend/.coverage

52 KB
Binary file not shown.

backend/TESTING.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Testing Guide
2+
3+
This guide explains how to run tests locally and in CI/CD.
4+
5+
## Quick Start
6+
7+
### 1. Install Test Dependencies
8+
9+
```bash
10+
uv sync --extra test
11+
```
12+
13+
### 2. Start Test Database
14+
15+
```bash
16+
# Start PostgreSQL test database in Docker
17+
./scripts/setup_test_db.sh
18+
```
19+
20+
This will:
21+
- Start PostgreSQL 16 in Docker on port 5433
22+
- Create the `devbin_test` database
23+
- Wait for the database to be ready
24+
25+
### 3. Run Tests
26+
27+
```bash
28+
# Run all tests
29+
uv run pytest
30+
31+
# Run only unit tests (no database required)
32+
uv run pytest tests/unit/ -v
33+
34+
# Run with coverage report
35+
uv run pytest --cov=app --cov-report=html
36+
37+
# Run in parallel (faster)
38+
uv run pytest -n auto
39+
40+
# Run specific test file
41+
uv run pytest tests/unit/test_token_utils.py -v
42+
```
43+
44+
## Test Structure
45+
46+
```
47+
tests/
48+
├── unit/ # Fast tests, no external dependencies
49+
├── integration/ # Tests with database and file system
50+
├── api/ # Full API endpoint tests
51+
└── security/ # Security-focused tests
52+
```
53+
54+
## Local Development
55+
56+
### Managing Test Database
57+
58+
```bash
59+
# Start test database
60+
docker-compose -f docker-compose.test.yml up -d
61+
62+
# Stop test database
63+
docker-compose -f docker-compose.test.yml down
64+
65+
# Clean up database and volumes
66+
docker-compose -f docker-compose.test.yml down -v
67+
68+
# View logs
69+
docker-compose -f docker-compose.test.yml logs -f
70+
```
71+
72+
### Test Database Connection
73+
74+
- **Host**: localhost
75+
- **Port**: 5433 (to avoid conflicts with dev database on 5432)
76+
- **Database**: devbin_test
77+
- **User**: postgres
78+
- **Password**: postgres
79+
- **Connection String**: `postgresql://postgres:postgres@localhost:5433/devbin_test`
80+
81+
### Environment Variables
82+
83+
Tests use environment variables from `pytest.ini` by default:
84+
85+
```ini
86+
APP_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/devbin_test
87+
APP_BASE_FOLDER_PATH=/tmp/devbin_test_files
88+
APP_DEBUG=true
89+
APP_ALLOW_CORS_WILDCARD=true
90+
```
91+
92+
You can override these by setting environment variables before running tests:
93+
94+
```bash
95+
export APP_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/my_test_db
96+
uv run pytest
97+
```
98+
99+
## CI/CD Setup
100+
101+
### GitHub Actions
102+
103+
The test database is automatically configured in `.github/workflows/test.yml`:
104+
105+
```yaml
106+
services:
107+
postgres:
108+
image: postgres:16
109+
env:
110+
POSTGRES_USER: postgres
111+
POSTGRES_PASSWORD: postgres
112+
POSTGRES_DB: devbin_test
113+
ports:
114+
- 5432:5432
115+
```
116+
117+
In CI, tests use port 5432 (the default PostgreSQL port in the service container).
118+
119+
### Running Tests in CI
120+
121+
```bash
122+
# CI sets APP_DATABASE_URL to use the service container
123+
pytest -v --cov=app --cov-report=xml
124+
coverage report --fail-under=80
125+
```
126+
127+
## Test Categories
128+
129+
### Unit Tests (Fast, Isolated)
130+
131+
```bash
132+
pytest tests/unit/ -m unit
133+
```
134+
135+
- No database or file system
136+
- Mock external dependencies
137+
- < 1ms per test
138+
- Test utilities, validators, pure functions
139+
140+
### Integration Tests
141+
142+
```bash
143+
pytest tests/integration/ -m integration
144+
```
145+
146+
- Real database with transaction rollback
147+
- File system operations (temp directories)
148+
- 10-100ms per test
149+
- Test service layer
150+
151+
### API Tests
152+
153+
```bash
154+
pytest tests/api/
155+
```
156+
157+
- Full HTTP request/response cycle
158+
- All middleware included
159+
- 50-200ms per test
160+
- Test endpoints, rate limiting, caching
161+
162+
## Coverage Reports
163+
164+
### View HTML Coverage Report
165+
166+
```bash
167+
uv run pytest --cov=app --cov-report=html
168+
open htmlcov/index.html # or xdg-open on Linux
169+
```
170+
171+
### Coverage Targets
172+
173+
- Overall: 80%+ (enforced in CI)
174+
- Critical modules: 90%+
175+
- `app/services/paste_service.py`
176+
- `app/utils/token_utils.py`
177+
- `app/api/subroutes/pastes.py`
178+
179+
## Troubleshooting
180+
181+
### Database Connection Errors
182+
183+
**Problem**: `connection refused` or `could not connect to server`
184+
185+
**Solution**:
186+
1. Check if test database is running: `docker ps | grep devbin_test`
187+
2. Start database: `./scripts/setup_test_db.sh`
188+
3. Check database logs: `docker-compose -f docker-compose.test.yml logs`
189+
190+
### Port Already in Use
191+
192+
**Problem**: Port 5433 is already in use
193+
194+
**Solution**:
195+
1. Change port in `docker-compose.test.yml`
196+
2. Update `pytest.ini` to match
197+
3. Restart database
198+
199+
### Tests Fail Randomly
200+
201+
**Problem**: Tests pass sometimes, fail other times (flaky tests)
202+
203+
**Solution**:
204+
1. Check if tests are properly isolated (no shared state)
205+
2. Verify database cleanup between tests
206+
3. Check for timing issues (use `freezegun` for time-based tests)
207+
208+
### Slow Tests
209+
210+
**Problem**: Tests take too long to run
211+
212+
**Solution**:
213+
1. Run tests in parallel: `pytest -n auto`
214+
2. Run only unit tests: `pytest tests/unit/`
215+
3. Run specific test file instead of entire suite
216+
4. Check for N+1 query issues in integration tests
217+
218+
## Best Practices
219+
220+
### Writing New Tests
221+
222+
1. **Use descriptive names**: `test_create_paste_with_valid_data_returns_200`
223+
2. **Test one thing**: Each test should verify one specific behavior
224+
3. **Use fixtures**: Reuse common setup via pytest fixtures
225+
4. **Clean up**: Tests should not leave artifacts (files, DB records)
226+
5. **Mark tests**: Use `@pytest.mark.unit` or `@pytest.mark.integration`
227+
228+
### Test Data
229+
230+
- Use `faker` for realistic test data (IPs, user agents, names)
231+
- Use `sample_paste_data` fixture for consistent paste creation
232+
- Create factory functions for complex test objects
233+
234+
### Mocking
235+
236+
- **Mock external services**: Time, disk usage checks, network calls
237+
- **Use real implementations**: Database, file system (with temp dirs)
238+
- **Mock sparingly**: Real implementations catch more bugs
239+
240+
## Continuous Integration
241+
242+
Tests run automatically on:
243+
- Push to `master` or `develop`
244+
- Pull requests
245+
246+
### CI Requirements
247+
248+
- All tests must pass
249+
- Coverage must not decrease
250+
- Coverage must be >= 80%
251+
- Linting must pass (ruff)
252+
253+
### Viewing CI Results
254+
255+
1. Go to GitHub Actions tab
256+
2. Click on the latest workflow run
257+
3. View test results and coverage report

backend/docker-compose.test.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
version: '3.8'
2+
3+
services:
4+
devbin_test_db:
5+
image: postgres:16
6+
container_name: devbin_test_db
7+
environment:
8+
POSTGRES_USER: postgres
9+
POSTGRES_PASSWORD: postgres
10+
POSTGRES_DB: devbin_test
11+
ports:
12+
- "5433:5432" # Use different port to avoid conflicts
13+
healthcheck:
14+
test: ["CMD-SHELL", "pg_isready -U postgres"]
15+
interval: 5s
16+
timeout: 5s
17+
retries: 5
18+
volumes:
19+
- devbin_test_data:/var/lib/postgresql/data
20+
21+
volumes:
22+
devbin_test_data:

backend/pytest.ini

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[pytest]
2+
asyncio_mode = auto
3+
testpaths = tests
4+
python_files = test_*.py
5+
python_classes = Test*
6+
python_functions = test_*
7+
plugins = pytest_configure
8+
addopts =
9+
-v
10+
--strict-markers
11+
--tb=short
12+
--cov=app
13+
--cov-report=term-missing
14+
--cov-report=html
15+
markers =
16+
unit: Unit tests (fast, isolated)
17+
integration: Integration tests (database, file system)
18+
security: Security-focused tests

backend/pytest_configure.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Pytest configuration plugin that sets environment variables early."""
2+
import os
3+
4+
5+
def pytest_configure(config):
6+
"""Set test environment variables before any test collection."""
7+
os.environ.setdefault("APP_DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5433/devbin_test")
8+
os.environ.setdefault("APP_BASE_FOLDER_PATH", "/tmp/devbin_test_files")
9+
os.environ.setdefault("APP_DEBUG", "true")
10+
# Use a test domain instead of wildcard to avoid validator issues
11+
os.environ.setdefault("APP_CORS_DOMAINS", '["http://test"]')
12+
os.environ.setdefault("APP_ALLOW_CORS_WILDCARD", "false")

backend/scripts/setup_test_db.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
# Setup test database for local development
3+
4+
set -e
5+
6+
echo "🔧 Setting up test database..."
7+
8+
# Detect docker compose command (docker-compose vs docker compose)
9+
if command -v docker-compose &> /dev/null; then
10+
DOCKER_COMPOSE="docker-compose"
11+
elif docker compose version &> /dev/null; then
12+
DOCKER_COMPOSE="docker compose"
13+
else
14+
echo "❌ Docker Compose not found. Please install Docker and Docker Compose."
15+
exit 1
16+
fi
17+
18+
echo "Using: $DOCKER_COMPOSE"
19+
20+
# Start test database
21+
echo "🐳 Starting PostgreSQL test database..."
22+
$DOCKER_COMPOSE -f docker-compose.test.yml up -d
23+
24+
# Wait for database to be ready
25+
echo "⏳ Waiting for database to be ready..."
26+
timeout=30
27+
elapsed=0
28+
while ! $DOCKER_COMPOSE -f docker-compose.test.yml exec -T devbin_test_db pg_isready -U postgres &> /dev/null; do
29+
if [ $elapsed -ge $timeout ]; then
30+
echo "❌ Database failed to start within ${timeout}s"
31+
$DOCKER_COMPOSE -f docker-compose.test.yml logs
32+
exit 1
33+
fi
34+
sleep 1
35+
elapsed=$((elapsed + 1))
36+
done
37+
38+
echo "✅ Test database is ready!"
39+
echo "📝 Connection string: postgresql://postgres:postgres@localhost:5433/devbin_test"
40+
echo ""
41+
echo "To run tests:"
42+
echo " uv run pytest"
43+
echo ""
44+
echo "To stop test database:"
45+
echo " $DOCKER_COMPOSE -f docker-compose.test.yml down"
46+
echo ""
47+
echo "To clean up test database and volumes:"
48+
echo " $DOCKER_COMPOSE -f docker-compose.test.yml down -v"

backend/tests/__init__.py

Whitespace-only changes.

backend/tests/api/__init__.py

Whitespace-only changes.

backend/tests/api/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Fixtures for API endpoint tests."""
2+
import pytest_asyncio
3+
from httpx import AsyncClient
4+
5+
6+
@pytest_asyncio.fixture
7+
async def authenticated_paste(test_client: AsyncClient, sample_paste_data):
8+
"""Create a paste and return it with auth tokens."""
9+
response = await test_client.post("/pastes", json=sample_paste_data)
10+
assert response.status_code == 200
11+
return response.json()

0 commit comments

Comments
 (0)