Skip to content

Testing Guide

Comprehensive guide for testing CareLog.

Testing Framework

CareLog uses pytest for testing with the following structure:

  • Unit tests for services
  • Integration tests for workflows
  • Fixture-based test data
  • Temporary databases for isolation

Running Tests

Run All Tests

bash
pytest tests/ -v

Run Specific Test File

bash
pytest tests/test_user_service.py -v

Run Specific Test

bash
pytest tests/test_user_service.py::test_create_user -v

Run with Coverage

bash
pytest tests/ --cov=core --cov-report=html

View coverage report: open htmlcov/index.html

Run Tests in Parallel

bash
pytest tests/ -n auto

Test Structure

Test File Organization

tests/
├── __init__.py
├── conftest.py              # Shared fixtures
├── test_auth.py             # Authentication tests
├── test_services.py         # Core service tests
├── test_user_service.py     # User management tests
├── test_health_log_service.py
├── test_appointment_service.py
├── test_diagnosis_prescription_service.py
├── test_emergency_call_advanced.py
├── test_visit_notification_service.py
├── test_audit_log_service.py
└── test_database_service.py

Naming Conventions

  • Test files: test_*.py
  • Test functions: test_*
  • Fixture functions: Descriptive names without test_ prefix

Writing Tests

Basic Test Structure

python
import pytest
from core.services.your_service import YourService

@pytest.fixture
def temp_db():
    """Create temporary database for testing"""
    import tempfile
    import os
    from core.services.database_service import DatabaseService
    
    # Create temp file
    fd, path = tempfile.mkstemp(suffix='.json')
    os.close(fd)
    
    # Initialize database
    db = DatabaseService(db_path=path)
    db.update("collection", {})
    
    yield db, path
    
    # Cleanup
    try:
        os.unlink(path)
    except:
        pass

def test_your_feature(temp_db):
    """Test your feature"""
    db, path = temp_db
    service = YourService(db_service=db)
    
    # Arrange
    expected_value = "test"
    
    # Act
    result = service.your_method(expected_value)
    
    # Assert
    assert result is not None
    assert result.field == expected_value

Test Fixtures

Shared Fixtures

Common fixtures are defined in conftest.py:

python
@pytest.fixture
def temp_db():
    """Temporary database for testing"""
    # Setup
    yield db, path
    # Teardown

Custom Fixtures

Create fixtures for test-specific setup:

python
@pytest.fixture
def sample_user(temp_db):
    """Create a sample user for testing"""
    db, path = temp_db
    service = UserService(db_service=db)
    user = service.create(
        name="Test User",
        firstName="Test",
        lastName="User",
        email="test@example.com",
        password="password",
        role="patient"
    )
    return user

Assertion Patterns

Basic Assertions

python
# Equality
assert result == expected

# Identity
assert result is not None

# Membership
assert item in collection

# Boolean
assert condition is True

None-Safe Assertions

Always check for None before accessing attributes:

python
# WRONG - May cause AttributeError
result = service.get("id")
assert result.field == "value"

# CORRECT - Safe pattern
result = service.get("id")
assert result is not None
assert result.field == "value"

Collection Assertions

python
# Length
assert len(results) == 3

# All items match condition
assert all(isinstance(item, Type) for item in results)

# Any item matches condition
assert any(item.field == "value" for item in results)

Test Categories

Unit Tests

Test individual components in isolation.

Example: Testing UserService

python
def test_create_user(temp_db):
    """Test user creation"""
    db, path = temp_db
    service = UserService(db_service=db)
    
    user = service.create(
        name="John Doe",
        firstName="John",
        lastName="Doe",
        email="john@example.com",
        password="password123",
        role="patient"
    )
    
    assert user is not None
    assert user.name == "John Doe"
    assert user.email == "john@example.com"
    assert user.role == "patient"

Integration Tests

Test interactions between components.

Example: Testing Appointment Workflow

python
def test_appointment_workflow(temp_db):
    """Test complete appointment workflow"""
    db, path = temp_db
    user_service = UserService(db_service=db)
    appointment_service = AppointmentService(db_service=db)
    
    # Create patient and doctor
    patient = user_service.create(..., role="patient")
    doctor = user_service.create(..., role="doctor")
    
    # Patient requests appointment
    appointment = appointment_service.create(
        patientID=patient.id,
        reason="Checkup",
        requestedDate="2025-10-26T10:00:00"
    )
    assert appointment.status == "Pending"
    
    # Doctor assigns appointment
    appointment_service.assign_doctor(appointment.appointmentID, doctor.id)
    updated = appointment_service.get(appointment.appointmentID)
    assert updated.doctorID == doctor.id
    
    # Complete appointment
    appointment_service.update_status(appointment.appointmentID, "Completed")
    final = appointment_service.get(appointment.appointmentID)
    assert final.status == "Completed"

Edge Case Tests

Test boundary conditions and error cases.

python
def test_get_nonexistent_user(temp_db):
    """Test getting user that doesn't exist"""
    db, path = temp_db
    service = UserService(db_service=db)
    
    result = service.get_by_id("nonexistent")
    assert result is None

def test_create_user_duplicate_email(temp_db):
    """Test creating user with duplicate email"""
    db, path = temp_db
    service = UserService(db_service=db)
    
    # Create first user
    service.create(..., email="test@example.com", ...)
    
    # Attempt to create duplicate
    with pytest.raises(Exception):  # Or specific exception
        service.create(..., email="test@example.com", ...)

Testing Best Practices

1. Arrange-Act-Assert Pattern

Structure tests clearly:

python
def test_feature():
    # Arrange - Set up test data
    service = YourService(db_service=db)
    test_data = "value"
    
    # Act - Execute the feature
    result = service.method(test_data)
    
    # Assert - Verify the result
    assert result == expected

2. One Concept Per Test

Each test should verify one specific behavior:

python
# GOOD - Single concept
def test_user_creation():
    user = service.create(...)
    assert user is not None

def test_user_email_validation():
    with pytest.raises(ValueError):
        service.create(..., email="invalid")

# BAD - Multiple concepts
def test_user():
    user = service.create(...)
    assert user is not None
    assert service.authenticate(...) is not None
    assert service.delete(...) is True

3. Descriptive Test Names

Use clear, descriptive names:

python
# GOOD
def test_create_user_with_valid_data():
    ...

def test_authenticate_returns_none_for_invalid_password():
    ...

# BAD
def test_user():
    ...

def test_auth():
    ...

4. Independent Tests

Tests should not depend on each other:

python
# GOOD - Each test is independent
def test_create():
    service.create(...)

def test_update():
    service.create(...)  # Create test data here
    service.update(...)

# BAD - Tests depend on execution order
created_user = None

def test_create():
    global created_user
    created_user = service.create(...)

def test_update():
    service.update(created_user.id, ...)  # Depends on previous test

5. Use Fixtures for Common Setup

Extract common setup to fixtures:

python
@pytest.fixture
def sample_patient(temp_db):
    db, path = temp_db
    service = UserService(db_service=db)
    return service.create(..., role="patient")

def test_with_patient(sample_patient):
    assert sample_patient.role == "patient"

6. Test Data Should Be Realistic

Use realistic test data:

python
# GOOD
def test_create_appointment():
    appointment = service.create(
        patientID="patient123",
        reason="Annual physical examination",
        requestedDate="2025-11-01T10:00:00"
    )

# AVOID
def test_create_appointment():
    appointment = service.create(
        patientID="1",
        reason="test",
        requestedDate="2025-01-01"
    )

Testing Specific Components

Testing Services with Database

python
def test_service_with_database(temp_db):
    db, path = temp_db
    service = YourService(db_service=db)
    
    # Create
    item = service.create(...)
    assert item is not None
    
    # Read
    retrieved = service.get(item.id)
    assert retrieved is not None
    assert retrieved.id == item.id
    
    # Update
    updated = service.update(item.id, field="new value")
    assert updated.field == "new value"
    
    # Delete
    success = service.delete(item.id)
    assert success is True
    assert service.get(item.id) is None

Testing Authentication

python
def test_password_hashing():
    hasher = PasswordHash()
    password = "secure_password_123"
    
    # Hash password
    hashed = hasher.hash(password)
    assert hashed != password
    assert len(hashed) > 0
    
    # Verify correct password
    assert hasher.verify(password, hashed) is True
    
    # Verify incorrect password
    assert hasher.verify("wrong", hashed) is False

def test_user_authentication(temp_db):
    db, path = temp_db
    service = UserService(db_service=db)
    
    # Create user
    service.create(..., email="test@example.com", password="pass123", ...)
    
    # Successful authentication
    user = service.authenticate("test@example.com", "pass123")
    assert user is not None
    
    # Failed authentication
    user = service.authenticate("test@example.com", "wrong")
    assert user is None

Testing with Mock Data

python
from unittest.mock import Mock, patch

def test_with_mock():
    # Mock external dependency
    mock_service = Mock()
    mock_service.get.return_value = {"data": "value"}
    
    # Test with mock
    result = your_function(mock_service)
    assert result == "expected"
    mock_service.get.assert_called_once()

Test Coverage

Measuring Coverage

bash
pytest tests/ --cov=core --cov-report=term-missing

Coverage Goals

  • Services: Aim for 90%+ coverage
  • Models: 80%+ coverage
  • Utils: 95%+ coverage
  • UI: Best effort (Streamlit is harder to test)

Coverage Report

bash
pytest tests/ --cov=core --cov-report=html
open htmlcov/index.html

Continuous Integration

GitHub Actions Example

yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.8'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov
      - name: Run tests
        run: pytest tests/ --cov=core --cov-report=xml
      - name: Upload coverage
        uses: codecov/codecov-action@v2

Debugging Tests

Running Tests in Debug Mode

bash
pytest tests/ -v -s  # -s shows print statements

Using pdb

python
def test_with_debugging():
    import pdb; pdb.set_trace()  # Breakpoint
    result = service.method()
    assert result is not None

Verbose Output

bash
pytest tests/ -vv  # Extra verbose

Common Testing Pitfalls

1. Not Checking for None

python
# WRONG
result = service.get("id")
assert result.field == "value"  # May raise AttributeError

# CORRECT
result = service.get("id")
assert result is not None
assert result.field == "value"

2. Shared Mutable State

python
# WRONG - Shared list between tests
test_data = []

def test_one():
    test_data.append("item")

def test_two():
    # test_data may have items from test_one!
    assert len(test_data) == 0  # May fail

# CORRECT - Fresh state each test
def test_one():
    test_data = []
    test_data.append("item")

def test_two():
    test_data = []
    assert len(test_data) == 0

3. Testing Implementation Instead of Behavior

python
# WRONG - Testing implementation
def test_internal_method():
    service._internal_method()  # Private method

# CORRECT - Testing behavior
def test_public_interface():
    result = service.public_method()
    assert result == expected

Next Steps