---
title: "Test Implementation: Core Unit Tests"
by: "pydapter-implementer"
created: "2025-05-04"
updated: "2025-05-04"
version: "1.0"
doc_type: TI
output_subdir: tis
description: "Detailed test implementation plan for pydapter's core unit testing framework"
---

# Test Implementation Plan: Core Unit Tests

## 1. Overview

### 1.1 Component Under Test

This test implementation plan covers the core components of pydapter:

- `Adapter` protocol and concrete implementations
- `AdapterRegistry` for managing adapter classes
- `Adaptable` mixin for model classes
- Asynchronous counterparts (`AsyncAdapter`, `AsyncAdapterRegistry`,
  `AsyncAdaptable`)

### 1.2 Test Approach

The testing approach combines:

- Unit tests for individual components
- Property-based tests for exhaustive validation
- Integration tests for component interactions
- Performance benchmarks for key operations

### 1.3 Key Testing Goals

- Verify protocol compliance for all adapter implementations
- Ensure registry functionality works correctly
- Validate mixin behavior for model classes
- Test error handling for all components
- Establish performance baselines

## 2. Test Environment

### 2.1 Test Framework

```
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-asyncio>=0.21.0
pytest-benchmark>=4.0.0
hypothesis>=6.82.0
testcontainers>=3.7.0
```

### 2.2 Mock Framework

```
unittest.mock (standard library)
```

### 2.3 Test Database

For database-dependent tests, we'll use testcontainers to provide ephemeral
containers:

- PostgreSQL for AsyncPostgresAdapter
- MongoDB for AsyncMongoAdapter
- Qdrant for AsyncQdrantAdapter

## 3. Unit Tests

### 3.1 Test Suite: Protocol Compliance

#### 3.1.1 Test Case: Adapter Protocol Compliance

**Purpose:** Verify that concrete adapter classes correctly implement the
Adapter protocol.

**Setup:**

```python
@pytest.fixture
def adapter_classes():
    from pydapter.adapters import JsonAdapter, CsvAdapter, TomlAdapter
    return [JsonAdapter, CsvAdapter, TomlAdapter]
```

**Test Implementation:**

```python
@pytest.mark.parametrize("adapter_cls", adapter_classes())
def test_adapter_protocol_compliance(adapter_cls):
    """Test that concrete adapters implement the Adapter protocol."""
    from pydapter.core import Adapter
    
    # Verify the adapter implements the protocol
    assert isinstance(adapter_cls, Adapter)
    
    # Verify required attributes exist
    assert hasattr(adapter_cls, "obj_key")
    assert isinstance(adapter_cls.obj_key, str)
    
    # Verify method signatures
    assert hasattr(adapter_cls, "from_obj")
    assert hasattr(adapter_cls, "to_obj")
```

#### 3.1.2 Test Case: AsyncAdapter Protocol Compliance

**Purpose:** Verify that concrete async adapter classes correctly implement the
AsyncAdapter protocol.

**Setup:**

```python
@pytest.fixture
def async_adapter_classes():
    from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
    from pydapter.extras.async_mongo_ import AsyncMongoAdapter
    from pydapter.extras.async_qdrant_ import AsyncQdrantAdapter
    return [AsyncPostgresAdapter, AsyncMongoAdapter, AsyncQdrantAdapter]
```

**Test Implementation:**

```python
@pytest.mark.parametrize("adapter_cls", async_adapter_classes())
def test_async_adapter_protocol_compliance(adapter_cls):
    """Test that concrete async adapters implement the AsyncAdapter protocol."""
    from pydapter.async_core import AsyncAdapter
    
    # Verify the adapter implements the protocol
    assert isinstance(adapter_cls, AsyncAdapter)
    
    # Verify required attributes exist
    assert hasattr(adapter_cls, "obj_key")
    assert isinstance(adapter_cls.obj_key, str)
    
    # Verify method signatures
    assert hasattr(adapter_cls, "from_obj")
    assert hasattr(adapter_cls, "to_obj")
```

### 3.2 Test Suite: AdapterRegistry

#### 3.2.1 Test Case: Registration and Retrieval

**Purpose:** Test registration and retrieval of adapters in the registry.

**Setup:**

```python
@pytest.fixture
def mock_adapter():
    class MockAdapter:
        obj_key = "mock"
        
        @classmethod
        def from_obj(cls, subj_cls, obj, /, *, many=False, **kw):
            return subj_cls()
        
        @classmethod
        def to_obj(cls, subj, /, *, many=False, **kw):
            return {}
    
    return MockAdapter
```

**Test Implementation:**

```python
def test_adapter_registry_registration(mock_adapter):
    """Test registration of adapters in the registry."""
    from pydapter.core import AdapterRegistry
    
    registry = AdapterRegistry()
    registry.register(mock_adapter)
    
    # Verify the adapter is registered
    assert registry.get("mock") == mock_adapter
```

#### 3.2.2 Test Case: Error Handling

**Purpose:** Test error handling for missing or invalid adapters.

**Setup:**

```python
@pytest.fixture
def invalid_adapters():
    """Collection of invalid adapter implementations for testing error handling."""
    
    class MissingKeyAdapter:
        """Adapter missing the required obj_key attribute."""
        
        @classmethod
        def from_obj(cls, subj_cls, obj, /, *, many=False, **kw):
            return subj_cls()
        
        @classmethod
        def to_obj(cls, subj, /, *, many=False, **kw):
            return {}
    
    class MissingMethodAdapter:
        """Adapter missing required methods."""
        obj_key = "invalid"
        
        # Missing from_obj and to_obj methods
    
    return {
        "missing_key": MissingKeyAdapter,
        "missing_method": MissingMethodAdapter,
    }
```

**Test Implementation:**

```python
def test_adapter_registry_error_handling(invalid_adapters):
    """Test error handling in AdapterRegistry."""
    from pydapter.core import AdapterRegistry
    
    registry = AdapterRegistry()
    
    # Test invalid adapter (missing obj_key)
    with pytest.raises(AttributeError, match="Adapter must define 'obj_key'"):
        registry.register(invalid_adapters["missing_key"])
    
    # Test retrieval of unregistered adapter
    with pytest.raises(KeyError, match="No adapter registered for 'nonexistent'"):
        registry.get("nonexistent")
```

### 3.3 Test Suite: Adaptable Mixin

#### 3.3.1 Test Case: Adapter Registration via Mixin

**Purpose:** Test registration of adapters via the Adaptable mixin.

**Setup:**

```python
@pytest.fixture
def model_class():
    from pydantic import BaseModel
    from pydapter.core import Adaptable
    
    class TestModel(Adaptable, BaseModel):
        id: int
        name: str
        value: float
    
    return TestModel
```

**Test Implementation:**

```python
def test_adaptable_registration(model_class, mock_adapter):
    """Test registration of adapters via the Adaptable mixin."""
    model_class.register_adapter(mock_adapter)
    
    # Verify the adapter is registered
    assert model_class._registry().get("mock") == mock_adapter
```

#### 3.3.2 Test Case: Round-Trip Conversion via Mixin

**Purpose:** Test round-trip conversion via the Adaptable mixin.

**Setup:**

```python
@pytest.fixture
def sample_model(model_class):
    return model_class(id=1, name="test", value=42.5)
```

**Test Implementation:**

```python
def test_adaptable_round_trip(sample_model):
    """Test round-trip conversion via the Adaptable mixin."""
    from pydapter.adapters import JsonAdapter
    
    sample_model.__class__.register_adapter(JsonAdapter)
    
    # Serialize the model
    serialized = sample_model.adapt_to(obj_key="json")
    
    # Deserialize the model
    deserialized = sample_model.__class__.adapt_from(serialized, obj_key="json")
    
    # Verify the deserialized model matches the original
    assert deserialized == sample_model
```

### 3.4 Test Suite: AsyncAdapterRegistry

#### 3.4.1 Test Case: Async Registration and Retrieval

**Purpose:** Test registration and retrieval of async adapters in the registry.

**Setup:**

```python
@pytest.fixture
def mock_async_adapter():
    class MockAsyncAdapter:
        obj_key = "mock_async"
        
        @classmethod
        async def from_obj(cls, subj_cls, obj, /, *, many=False, **kw):
            return subj_cls()
        
        @classmethod
        async def to_obj(cls, subj, /, *, many=False, **kw):
            return {}
    
    return MockAsyncAdapter
```

**Test Implementation:**

```python
def test_async_adapter_registry_registration(mock_async_adapter):
    """Test registration of async adapters in the registry."""
    from pydapter.async_core import AsyncAdapterRegistry
    
    registry = AsyncAdapterRegistry()
    registry.register(mock_async_adapter)
    
    # Verify the adapter is registered
    assert registry.get("mock_async") == mock_async_adapter
```

### 3.5 Test Suite: AsyncAdaptable Mixin

#### 3.5.1 Test Case: Async Adapter Registration via Mixin

**Purpose:** Test registration of async adapters via the AsyncAdaptable mixin.

**Setup:**

```python
@pytest.fixture
def async_model_class():
    from pydantic import BaseModel
    from pydapter.async_core import AsyncAdaptable
    
    class TestAsyncModel(AsyncAdaptable, BaseModel):
        id: int
        name: str
        value: float
    
    return TestAsyncModel
```

**Test Implementation:**

```python
def test_async_adaptable_registration(async_model_class, mock_async_adapter):
    """Test registration of async adapters via the AsyncAdaptable mixin."""
    async_model_class.register_async_adapter(mock_async_adapter)
    
    # Verify the adapter is registered
    assert async_model_class._areg().get("mock_async") == mock_async_adapter
```

#### 3.5.2 Test Case: Async Round-Trip Conversion via Mixin

**Purpose:** Test round-trip conversion via the AsyncAdaptable mixin.

**Setup:**

```python
@pytest.fixture
def async_sample_model(async_model_class):
    return async_model_class(id=1, name="test", value=42.5)
```

**Test Implementation:**

```python
@pytest.mark.asyncio
async def test_async_adaptable_round_trip(async_sample_model, mock_async_adapter):
    """Test round-trip conversion via the AsyncAdaptable mixin."""
    from unittest.mock import AsyncMock
    
    # Mock the adapter methods
    mock_async_adapter.from_obj = AsyncMock(return_value=async_sample_model)
    mock_async_adapter.to_obj = AsyncMock(return_value={"id": 1, "name": "test", "value": 42.5})
    
    async_sample_model.__class__.register_async_adapter(mock_async_adapter)
    
    # Serialize the model
    serialized = await async_sample_model.adapt_to_async(obj_key="mock_async")
    
    # Deserialize the model
    deserialized = await async_sample_model.__class__.adapt_from_async(
        serialized, obj_key="mock_async"
    )
    
    # Verify the deserialized model matches the original
    assert deserialized == async_sample_model
```

## 4. Property-Based Tests

### 4.1 Test Suite: Round-Trip Conversion

#### 4.1.1 Test Case: JsonAdapter Round-Trip

**Purpose:** Test round-trip conversion through JsonAdapter with a wide range of
inputs.

**Setup:**

```python
from hypothesis import given, strategies as st

@pytest.fixture
def model_factory():
    """Factory for creating test models with adapters registered."""
    from pydantic import BaseModel
    from pydapter.core import Adaptable
    from pydapter.adapters import JsonAdapter
    
    def create_model(**kw):
        class TestModel(Adaptable, BaseModel):
            id: int
            name: str
            value: float
        
        TestModel.register_adapter(JsonAdapter)
        return TestModel(**kw)
    
    return create_model
```

**Test Implementation:**

```python
@given(
    id=st.integers(),
    name=st.text(min_size=1, max_size=50),
    value=st.floats(allow_nan=False, allow_infinity=False)
)
def test_json_adapter_roundtrip(id, name, value, model_factory):
    """Test that objects can be round-tripped through the JsonAdapter."""
    model = model_factory(id=id, name=name, value=value)
    serialized = model.adapt_to(obj_key="json")
    deserialized = model.__class__.adapt_from(serialized, obj_key="json")
    assert deserialized == model
```

#### 4.1.2 Test Case: CsvAdapter Round-Trip

**Purpose:** Test round-trip conversion through CsvAdapter with a wide range of
inputs.

**Test Implementation:**

```python
@given(
    id=st.integers(),
    name=st.text(min_size=1, max_size=50).filter(lambda x: "," not in x),
    value=st.floats(allow_nan=False, allow_infinity=False)
)
def test_csv_adapter_roundtrip(id, name, value, model_factory):
    """Test that objects can be round-tripped through the CsvAdapter."""
    from pydapter.adapters import CsvAdapter
    
    model = model_factory(id=id, name=name, value=value)
    model.__class__.register_adapter(CsvAdapter)
    
    serialized = model.adapt_to(obj_key="csv")
    deserialized = model.__class__.adapt_from(serialized, obj_key="csv")
    assert deserialized == model
```

### 4.2 Test Suite: Edge Cases

#### 4.2.1 Test Case: Empty Values

**Purpose:** Test handling of empty values in adapters.

**Test Implementation:**

```python
@given(
    name=st.one_of(st.just(""), st.text(min_size=1, max_size=50)),
    value=st.one_of(st.just(0.0), st.floats(allow_nan=False, allow_infinity=False))
)
def test_adapter_empty_values(name, value, model_factory):
    """Test handling of empty values in adapters."""
    from pydapter.adapters import JsonAdapter
    
    model = model_factory(id=0, name=name, value=value)
    model.__class__.register_adapter(JsonAdapter)
    
    serialized = model.adapt_to(obj_key="json")
    deserialized = model.__class__.adapt_from(serialized, obj_key="json")
    assert deserialized == model
```

#### 4.2.2 Test Case: Special Characters

**Purpose:** Test handling of special characters in adapters.

**Test Implementation:**

```python
@given(
    name=st.text(alphabet=st.characters(blacklist_categories=('Cs',)), min_size=1, max_size=50),
    value=st.floats(allow_nan=False, allow_infinity=False)
)
def test_adapter_special_characters(name, value, model_factory):
    """Test handling of special characters in adapters."""
    from pydapter.adapters import JsonAdapter
    
    model = model_factory(id=1, name=name, value=value)
    model.__class__.register_adapter(JsonAdapter)
    
    serialized = model.adapt_to(obj_key="json")
    deserialized = model.__class__.adapt_from(serialized, obj_key="json")
    assert deserialized == model
```

## 5. Integration Tests

### 5.1 Test Suite: Multiple Adapters

#### 5.1.1 Test Case: Multiple Adapter Registration

**Purpose:** Test registration and use of multiple adapters with a single model.

**Test Implementation:**

```python
def test_multiple_adapter_registration(model_factory):
    """Test registration and use of multiple adapters with a single model."""
    from pydapter.adapters import JsonAdapter, CsvAdapter, TomlAdapter
    
    model = model_factory(id=1, name="test", value=42.5)
    model_cls = model.__class__
    
    # Register multiple adapters
    model_cls.register_adapter(JsonAdapter)
    model_cls.register_adapter(CsvAdapter)
    model_cls.register_adapter(TomlAdapter)
    
    # Test each adapter
    for adapter_key in ["json", "csv", "toml"]:
        serialized = model.adapt_to(obj_key=adapter_key)
        deserialized = model_cls.adapt_from(serialized, obj_key=adapter_key)
        assert deserialized == model
```

### 5.2 Test Suite: Async Database Adapters

#### 5.2.1 Test Case: AsyncPostgresAdapter

**Purpose:** Test AsyncPostgresAdapter with a PostgreSQL database.

**Setup:**

```python
@pytest.fixture(scope="session")
def pg_container():
    """Provide a PostgreSQL container for testing."""
    from testcontainers.postgres import PostgresContainer
    
    container = PostgresContainer("postgres:16-alpine")
    container.start()
    yield container
    container.stop()

@pytest.fixture
def pg_url(pg_container):
    """Get the PostgreSQL connection URL."""
    url = pg_container.get_connection_url()
    return url.replace("postgresql://", "postgresql+asyncpg://")
```

**Test Implementation:**

```python
@pytest.mark.asyncio
@pytest.mark.integration
async def test_async_postgres_adapter(async_model_factory, pg_url):
    """Test AsyncPostgresAdapter with a PostgreSQL database."""
    from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
    
    model = async_model_factory(id=1, name="test", value=42.5)
    model.__class__.register_async_adapter(AsyncPostgresAdapter)
    
    # Store in database
    await model.adapt_to_async(
        obj_key="async_pg",
        dsn=pg_url,
        table="test_table"
    )
    
    # Retrieve from database
    retrieved = await model.__class__.adapt_from_async(
        {"dsn": pg_url, "table": "test_table", "selectors": {"id": 1}},
        obj_key="async_pg"
    )
    
    assert retrieved == model
```

## 6. Error Handling Tests

### 6.1 Test Suite: Invalid Inputs

#### 6.1.1 Test Case: Invalid JSON

**Purpose:** Test handling of invalid JSON input.

**Test Implementation:**

```python
def test_json_adapter_invalid_input(model_factory):
    """Test handling of invalid JSON input."""
    from pydapter.adapters import JsonAdapter
    
    model_cls = model_factory(id=1, name="test", value=42.5).__class__
    model_cls.register_adapter(JsonAdapter)
    
    # Test invalid JSON
    with pytest.raises(Exception):  # Could be json.JSONDecodeError or ValidationError
        model_cls.adapt_from("{invalid json}", obj_key="json")
```

#### 6.1.2 Test Case: Missing Required Fields

**Purpose:** Test handling of missing required fields.

**Test Implementation:**

```python
def test_adapter_missing_fields(model_factory):
    """Test handling of missing required fields."""
    from pydapter.adapters import JsonAdapter
    
    model_cls = model_factory(id=1, name="test", value=42.5).__class__
    model_cls.register_adapter(JsonAdapter)
    
    # Test missing required fields
    with pytest.raises(Exception):  # Likely ValidationError
        model_cls.adapt_from('{"id": 1}', obj_key="json")
```

## 7. Performance Tests

### 7.1 Test Suite: Serialization Benchmarks

#### 7.1.1 Test Case: JsonAdapter Serialization

**Purpose:** Benchmark JsonAdapter serialization performance.

**Test Implementation:**

```python
def test_json_adapter_serialization_benchmark(benchmark, sample_model):
    """Benchmark JsonAdapter serialization performance."""
    from pydapter.adapters import JsonAdapter
    
    sample_model.__class__.register_adapter(JsonAdapter)
    
    # Benchmark serialization
    benchmark(sample_model.adapt_to, obj_key="json")
```

### 7.2 Test Suite: Deserialization Benchmarks

#### 7.2.1 Test Case: JsonAdapter Deserialization

**Purpose:** Benchmark JsonAdapter deserialization performance.

**Test Implementation:**

```python
def test_json_adapter_deserialization_benchmark(benchmark, sample_model):
    """Benchmark JsonAdapter deserialization performance."""
    from pydapter.adapters import JsonAdapter
    
    model_cls = sample_model.__class__
    model_cls.register_adapter(JsonAdapter)
    
    # Serialize the model first
    serialized = sample_model.adapt_to(obj_key="json")
    
    # Benchmark deserialization
    benchmark(model_cls.adapt_from, serialized, obj_key="json")
```

### 7.3 Test Suite: Registry Benchmarks

#### 7.3.1 Test Case: Registry Lookup

**Purpose:** Benchmark registry lookup performance.

**Test Implementation:**

```python
def test_registry_lookup_benchmark(benchmark):
    """Benchmark registry lookup performance."""
    from pydapter.core import AdapterRegistry
    from pydapter.adapters import JsonAdapter, CsvAdapter, TomlAdapter
    
    registry = AdapterRegistry()
    registry.register(JsonAdapter)
    registry.register(CsvAdapter)
    registry.register(TomlAdapter)
    
    # Benchmark lookup
    benchmark(registry.get, "json")
```

## 8. Mock Implementation Details

```python
class MockAdapter:
    """Mock adapter for testing."""
    obj_key = "mock"
    
    @classmethod
    def from_obj(cls, subj_cls, obj, /, *, many=False, **kw):
        """Convert from object to model."""
        if many:
            return [subj_cls(id=item["id"], name=item["name"], value=item["value"]) for item in obj]
        return subj_cls(id=obj["id"], name=obj["name"], value=obj["value"])
    
    @classmethod
    def to_obj(cls, subj, /, *, many=False, **kw):
        """Convert from model to object."""
        if many:
            return [{"id": item.id, "name": item.name, "value": item.value} for item in subj]
        return {"id": subj.id, "name": subj.name, "value": subj.value}

class MockAsyncAdapter:
    """Mock async adapter for testing."""
    obj_key = "mock_async"
    
    @classmethod
    async def from_obj(cls, subj_cls, obj, /, *, many=False, **kw):
        """Convert from object to model asynchronously."""
        if many:
            return [subj_cls(id=item["id"], name=item["name"], value=item["value"]) for item in obj]
        return subj_cls(id=obj["id"], name=obj["name"], value=obj["value"])
    
    @classmethod
    async def to_obj(cls, subj, /, *, many=False, **kw):
        """Convert from model to object asynchronously."""
        if many:
            return [{"id": item.id, "name": item.name, "value": item.value} for item in subj]
        return {"id": subj.id, "name": subj.name, "value": subj.value}
```

## 9. Test Data

```python
# Sample model data
sample_data = {
    "simple": {"id": 1, "name": "test", "value": 42.5},
    "empty": {"id": 0, "name": "", "value": 0.0},
    "special": {"id": 999, "name": "Special Characters: !@#$%^&*()", "value": -123.456},
}

# Sample async model data
async_sample_data = {
    "simple": {"id": 1, "name": "test", "value": 42.5, "embedding": [0.1, 0.2, 0.3, 0.4, 0.5]},
    "empty": {"id": 0, "name": "", "value": 0.0, "embedding": [0.0, 0.0, 0.0, 0.0, 0.0]},
    "special": {"id": 999, "name": "Special Characters: !@#$%^&*()", "value": -123.456, "embedding": [0.9, 0.8, 0.7, 0.6, 0.5]},
}
```

## 10. Helper Functions

```python
def validate_schema(adapter_cls, expected_schema):
    """Validate that an adapter's schema matches the expected schema."""
    actual_schema = adapter_cls.get_schema()
    assert actual_schema == expected_schema
    
def check_error_handling(func, error_inputs, expected_exception):
    """Test that a function raises the expected exception for specific inputs."""
    for error_input in error_inputs:
        with pytest.raises(expected_exception):
            func(error_input)
```

## 11. Test Coverage Targets

- **Line Coverage Target:** 90%
- **Branch Coverage Target:** 85%
- **Critical Modules:** 95% coverage for core.py and async_core.py

## 12. Continuous Integration

```yaml
name: Test
on: [push, pull_request]
jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .
          pip install pytest pytest-cov pytest-asyncio pytest-benchmark hypothesis testcontainers
      - name: Run tests
        run: |
          pytest tests/ --cov=pydapter --cov-report=xml
      - name: Upload coverage
        uses: codecov/codecov-action@v3
```

## 13. Notes and Caveats

### 13.1 Known Limitations

- Container-based tests may be slow and could be skipped in development
  environments
- Property-based tests may generate a large number of examples, which can be
  limited in development
- Async tests may be flaky due to timing issues

### 13.2 Future Improvements

- Add more comprehensive property-based tests for all adapters
- Implement stateful testing for complex workflows
- Add more detailed performance benchmarks
- Improve container-based testing with better fallback mechanisms
