Skip to main content

Testing

Guide to testing Batchivo's backend and frontend.

Backend Testing

Running Tests

cd backend

# All tests
poetry run pytest

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

# Verbose output
poetry run pytest -v

# Specific file
poetry run pytest tests/test_spools.py

# Specific test
poetry run pytest tests/test_spools.py::test_create_spool -v

Test Structure

backend/tests/
├── conftest.py # Fixtures and setup
├── test_auth.py # Authentication tests
├── test_spools.py # Spool CRUD tests
├── test_products.py # Product tests
├── test_production.py # Production run tests
└── integration/ # Integration tests
└── test_workflows.py

Writing Tests

import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_create_spool(client: AsyncClient, auth_headers: dict):
"""Test creating a new spool."""
response = await client.post(
"/api/v1/spools",
json={
"material_type": "PLA",
"color": "Black",
"diameter_mm": 1.75,
"net_weight_grams": 1000,
},
headers=auth_headers,
)

assert response.status_code == 201
data = response.json()
assert data["material_type"] == "PLA"
assert data["color"] == "Black"

Fixtures

Common fixtures in conftest.py:

@pytest.fixture
async def client(app):
"""Async HTTP client for testing."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client

@pytest.fixture
async def auth_headers(client):
"""Get authentication headers."""
response = await client.post(
"/api/v1/auth/login",
json={"email": "test@example.com", "password": "password"},
)
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}

@pytest.fixture
async def test_spool(client, auth_headers):
"""Create a test spool."""
response = await client.post(
"/api/v1/spools",
json={...},
headers=auth_headers,
)
return response.json()

Database Testing

Tests use a separate test database:

@pytest.fixture(scope="session")
async def test_db():
"""Create test database."""
engine = create_async_engine(TEST_DATABASE_URL)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)

Frontend Testing

Running Tests

cd frontend

# All tests
npm run test

# Watch mode
npm run test:watch

# Coverage
npm run test:coverage

# Specific file
npm run test -- SpoolList.test.tsx

Test Structure

frontend/src/
├── components/
│ ├── SpoolList.tsx
│ └── SpoolList.test.tsx
├── hooks/
│ ├── useSpools.ts
│ └── useSpools.test.ts
└── __tests__/
└── integration/

Writing Tests

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SpoolList } from './SpoolList';

describe('SpoolList', () => {
it('renders spool items', async () => {
render(<SpoolList />);

await waitFor(() => {
expect(screen.getByText('PLA Black')).toBeInTheDocument();
});
});

it('filters by material type', async () => {
const user = userEvent.setup();
render(<SpoolList />);

await user.click(screen.getByRole('combobox', { name: /material/i }));
await user.click(screen.getByRole('option', { name: 'PETG' }));

await waitFor(() => {
expect(screen.queryByText('PLA Black')).not.toBeInTheDocument();
});
});
});

Mocking API Calls

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
rest.get('/api/v1/spools', (req, res, ctx) => {
return res(ctx.json({
items: [
{ id: '1', material_type: 'PLA', color: 'Black' },
],
total: 1,
}));
}),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Integration Testing

End-to-End Workflows

@pytest.mark.asyncio
async def test_production_run_workflow(client, auth_headers):
"""Test complete production run workflow."""

# 1. Create spool
spool = await create_spool(client, auth_headers)

# 2. Create product
product = await create_product(client, auth_headers)

# 3. Start production run
run = await client.post(
"/api/v1/production-runs",
json={
"printer_name": "Test Printer",
"estimated_print_time_hours": 2,
},
headers=auth_headers,
)
run_id = run.json()["id"]

# 4. Add items and materials
await client.post(f"/api/v1/production-runs/{run_id}/items", ...)
await client.post(f"/api/v1/production-runs/{run_id}/materials", ...)

# 5. Complete run
result = await client.post(f"/api/v1/production-runs/{run_id}/complete")

assert result.status_code == 200
assert result.json()["status"] == "completed"

CI/CD Testing

Tests run automatically on:

  • Pull requests
  • Pushes to main
  • Release tags

See .github/workflows/ci.yml for configuration.