Skip to main content

Overview

VidaPlus API uses pytest as its testing framework, with additional support for async testing via pytest-asyncio and coverage reporting with pytest-cov. The test suite ensures API endpoints, authentication, database operations, and business logic work correctly.

Test Configuration

Dependencies

The project includes these testing dependencies in pyproject.toml:
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.5"
pytest-cov = "^6.0.0"
pytest-asyncio = "^0.26.0"

Pytest Configuration

The pytest configuration is defined in pyproject.toml:
[tool.pytest.ini_options]
pythonpath = "."
addopts = '-p no:warnings'
asyncio_default_fixture_loop_scope = "function"
Configuration details:
  • pythonpath = "." - Adds project root to Python path
  • addopts = '-p no:warnings' - Suppresses warnings during test runs
  • asyncio_default_fixture_loop_scope = "function" - Sets async fixture scope to function level

Running Tests

Basic Test Execution

Run all tests:
pytest

Using Taskipy

The project includes pre-configured test tasks using taskipy:
task test
This command:
  1. Runs linting (task lint) as a pre-test step
  2. Executes tests with coverage: pytest -s -x --cov=vidaplus -vv
  3. Generates HTML coverage report as a post-test step
Task flags explained:
  • -s - Show print statements and output during tests
  • -x - Stop on first test failure
  • --cov=vidaplus - Generate coverage report for the vidaplus package
  • -vv - Very verbose output

Running Specific Tests

pytest tests/test_paciente.py

Coverage Reports

Generate coverage report:
pytest --cov=vidaplus --cov-report=html
View the HTML report:
open htmlcov/index.html  # macOS
xdg-open htmlcov/index.html  # Linux
start htmlcov/index.html  # Windows

Test Structure

Test Directory Layout

tests/
├── __init__.py
├── conftest.py              # Shared fixtures
├── test_auth.py             # Authentication tests
├── test_consulta.py         # Consulta endpoint tests
├── test_db.py               # Database tests
├── test_paciente.py         # Paciente endpoint tests
├── test_profissional.py     # Profissional endpoint tests
├── test_prontuario.py       # Prontuário endpoint tests
└── test_security.py         # Security/password tests

Fixtures (conftest.py)

The tests/conftest.py file contains shared fixtures used across all tests:
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from vidaplus.app import app
from vidaplus.database import get_session
from vidaplus.models.models import table_registry

@pytest.fixture
def session():
    """Create an in-memory SQLite database for testing."""
    engine = create_engine(
        'sqlite:///:memory:',
        connect_args={'check_same_thread': False},
        poolclass=StaticPool,
    )
    table_registry.metadata.create_all(engine)

    with Session(engine) as session:
        yield session

    table_registry.metadata.drop_all(engine)

@pytest.fixture
def client(session):
    """Create a test client with database session override."""
    def get_session_override():
        return session

    with TestClient(app) as client:
        app.dependency_overrides[get_session] = get_session_override
        yield client

    app.dependency_overrides.clear()
Key fixtures:

session

Provides an in-memory SQLite database for isolated testing

client

FastAPI TestClient with dependency overrides for the session

paciente_user

Creates a test patient user with hashed password

profissional_user

Creates a test healthcare professional user

admin_user

Creates a test admin user with superuser permissions

token_pacient

Generates JWT token for patient authentication

token_profissional

Generates JWT token for professional authentication

token_admin

Generates JWT token for admin authentication

User Fixtures

The conftest includes fixtures for creating test users:
@pytest.fixture
def paciente_user(session, mock_db_date):
    """Create a test patient user."""
    with mock_db_date(model=PacienteUser) as date:
        senha = 'maria123'
        user = PacienteUser(
            nome='Maria',
            email='[email protected]',
            senha=get_password_hash(senha),
            telefone='987654322',
            tipo='PACIENTE',
            is_active=True,
            is_superuser=False,
            cpf='98765432100',
            data_nascimento=date,
            endereco='Rua B',
            complemento='Casa',
            numero=456,
            bairro='Jardim',
            cidade='Rio de Janeiro',
            estado='RJ',
            cep='87654321',
        )
        session.add(user)
        session.commit()
        session.refresh(user)

        user.clean_password = senha
        return user

Writing Tests

Authentication Tests

Example from tests/test_auth.py:
from http import HTTPStatus

def test_get_token_paciente(client, paciente_user):
    """Test getting JWT token for a patient user."""
    response = client.post(
        '/auth/token',
        data={
            'username': paciente_user.email,
            'password': paciente_user.clean_password,
        },
    )
    token = response.json()

    assert response.status_code == HTTPStatus.OK
    assert 'access_token' in token
    assert 'token_type' in token

def test_get_token_profissional(client, profissional_user):
    """Test getting JWT token for a professional user."""
    response = client.post(
        '/auth/token',
        data={
            'username': profissional_user.email,
            'password': profissional_user.clean_password,
        },
    )
    token = response.json()

    assert response.status_code == HTTPStatus.OK
    assert 'access_token' in token
    assert 'token_type' in token

CRUD Endpoint Tests

Example from tests/test_paciente.py:
from http import HTTPStatus
from vidaplus.schemas.paciente_schema import PacienteUserPublic

def test_create_paciente(client, token_admin):
    """Test creating a new patient (admin only)."""
    response = client.post(
        '/pacientes/',
        json={
            'nome': 'Alice Silva',
            'email': '[email protected]',
            'senha': 'secret',
            'telefone': '123456789',
            'cpf': '123.456.789-00',
            'data_nascimento': '2002-01-01',
            'endereco': 'Rua Exemplo, 123',
            'complemento': 'Apto 101',
            'numero': 123,
            'bairro': 'Centro',
            'cidade': 'São Paulo',
            'estado': 'SP',
            'cep': '12345-678',
            'tipo': 'PACIENTE',
            'is_active': True,
            'is_superuser': False,
        },
        headers={'Authorization': f'Bearer {token_admin}'},
    )

    assert response.status_code == HTTPStatus.CREATED
    assert response.json()['nome'] == 'Alice Silva'
    assert response.json()['email'] == '[email protected]'

def test_read_pacientes_with_pacientes(client, paciente_user, token_admin):
    """Test listing patients when patients exist."""
    user_schema = PacienteUserPublic.model_validate(paciente_user).model_dump(
        mode='json'
    )

    response = client.get(
        '/pacientes/', 
        headers={'Authorization': f'Bearer {token_admin}'}
    )

    assert response.status_code == HTTPStatus.OK
    assert response.json() == {'pacientes': [user_schema]}

def test_delete_user(client, paciente_user, token_pacient):
    """Test deleting a patient user."""
    response = client.delete(
        f'/pacientes/{paciente_user.id}',
        headers={'Authorization': f'Bearer {token_pacient}'},
    )
    assert response.status_code == HTTPStatus.OK
    assert response.json() == {'message': 'User deleted'}

Error Handling Tests

def test_get_paciente_not_found(client, token_pacient):
    """Test getting non-existent patient returns 404."""
    response = client.get(
        '/pacientes/999', 
        headers={'Authorization': f'Bearer {token_pacient}'}
    )
    assert response.status_code == HTTPStatus.NOT_FOUND
    assert response.json() == {'detail': 'User not found'}

def test_update_integrity_error(client, paciente_user, token_pacient):
    """Test updating with duplicate email/CPF returns conflict."""
    response_update = client.put(
        f'/pacientes/{paciente_user.id}',
        headers={'Authorization': f'Bearer {token_pacient}'},
        json={
            'nome': 'Updated Name',
            'email': '[email protected]',  # Duplicate email
            # ... other fields
        },
    )

    assert response_update.status_code == HTTPStatus.CONFLICT
    assert response_update.json() == {'detail': 'CPF or Email already exists'}

Best Practices

1

Use Descriptive Test Names

Write test function names that clearly describe what is being tested:
# Good
def test_create_paciente_requires_admin_token():
    ...

# Bad
def test_create():
    ...
2

Test One Thing Per Test

Each test should verify a single behavior:
# Good - separate tests
def test_create_paciente_returns_201():
    ...

def test_create_paciente_returns_correct_data():
    ...

# Bad - testing multiple things
def test_create_paciente():
    # Tests status code, response data, database state, etc.
    ...
3

Use Fixtures for Setup

Leverage pytest fixtures to avoid code duplication:
def test_update_paciente(client, paciente_user, token_pacient):
    # paciente_user and token are provided by fixtures
    response = client.put(
        f'/pacientes/{paciente_user.id}',
        headers={'Authorization': f'Bearer {token_pacient}'},
        json={...}
    )
4

Assert Expected Behavior

Use specific assertions:
# Good
assert response.status_code == HTTPStatus.CREATED
assert response.json()['email'] == '[email protected]'

# Bad
assert response.status_code == 201  # Use HTTPStatus enum
assert 'email' in response.json()  # Too vague
5

Clean Up After Tests

The session fixture automatically cleans up by dropping tables after each test. For custom resources, use yield fixtures:
@pytest.fixture
def temp_file():
    file = create_temp_file()
    yield file
    file.cleanup()  # Cleanup after test

Running Tests in CI/CD

For continuous integration, use this GitHub Actions example:
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.13'
    
    - name: Install Poetry
      run: pipx install poetry
    
    - name: Install dependencies
      run: poetry install
    
    - name: Run tests
      run: poetry run task test
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml

Troubleshooting

Import Errors

If you encounter import errors, ensure:
  1. pythonpath = "." is set in pyproject.toml
  2. You’re running tests from the project root
  3. All __init__.py files exist

Async Tests Failing

For async test functions, use the pytest.mark.asyncio decorator:
import pytest

@pytest.mark.asyncio
async def test_async_endpoint(client):
    response = await client.get('/async-endpoint')
    assert response.status_code == 200

Database Lock Errors

SQLite in-memory databases use StaticPool to prevent threading issues. If you still encounter lock errors, ensure you’re not accessing the database outside the session context.

Test Discovery Issues

Pytest discovers tests by:
  • Files starting with test_ or ending with _test.py
  • Functions starting with test_
  • Classes starting with Test
Ensure your test files follow this convention.

Next Steps

Contributing

Learn how to contribute to VidaPlus API

API Reference

Explore the complete API documentation

Build docs developers (and LLMs) love