# Testing Conventions

## Overview

The SDK uses multiple layers of testing to ensure correctness, reliability, and maintainability. This document defines test organization and patterns.

## Test Organization

### Directory Structure

```
tests/
├── specification/              # Protocol compliance, API surface
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_protocol_compliance.py
│   └── test_api_surface.py
├── unit/                       # Fast, isolated tests
│   ├── __init__.py
│   ├── conftest.py
│   ├── http/
│   │   ├── test_client.py
│   │   ├── test_async_client.py
│   │   └── test_rate_limiter.py
│   ├── models/
│   │   └── test_index.py
│   └── client/
│       └── test_client.py
├── integration/                # Real API tests
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_indexes.py
│   └── test_inference.py
└── examples/                   # Example code tests
    ├── __init__.py
    └── test_quickstart.py
```

## Test Types

### Specification Tests

**Purpose**: Verify implementations satisfy protocols

```python
# tests/specification/test_protocol_compliance.py
from pinecone.__interface__.client import ClientProtocol
from pinecone import Pinecone

def test_client_implements_protocol():
    """Client must implement ClientProtocol."""
    pc = Pinecone(api_key="test")
    assert isinstance(pc, ClientProtocol)

def test_client_has_required_methods():
    """Client must have all required methods."""
    pc = Pinecone(api_key="test")

    # Check methods exist
    assert hasattr(pc, "list_indexes")
    assert hasattr(pc, "create_index")
    assert hasattr(pc, "delete_index")

    # Check signatures
    import inspect
    sig = inspect.signature(pc.list_indexes)
    assert len(sig.parameters) == 0
    assert sig.return_annotation == list[str]
```

### Unit Tests

**Purpose**: Test individual components in isolation

```python
# tests/unit/http/test_client.py
import pytest
import httpx
import respx

from pinecone._internal.http.client import HTTPClient
from pinecone._internal.errors import RateLimitError

@respx.mock
def test_http_client_get():
    """HTTPClient GET request works."""
    respx.get("https://api.pinecone.io/indexes").mock(
        return_value=httpx.Response(200, json={"indexes": []})
    )

    client = HTTPClient(api_key="test")
    response = client.get("/indexes")

    assert response.status_code == 200
    assert response.json() == {"indexes": []}

@respx.mock
def test_http_client_retries_on_429():
    """HTTPClient retries on rate limit."""
    # First request fails, second succeeds
    respx.get("https://api.pinecone.io/indexes").mock(
        side_effect=[
            httpx.Response(429, headers={"Retry-After": "1"}),
            httpx.Response(200, json={"indexes": []})
        ]
    )

    client = HTTPClient(api_key="test")
    response = client.get("/indexes")

    assert response.status_code == 200
    assert len(respx.calls) == 2  # Verify retry happened
```

### Integration Tests

**Purpose**: Test against real API (requires API key)

```python
# tests/integration/test_indexes.py
import pytest
import os

from pinecone import Pinecone

# Skip if no API key
pytestmark = pytest.mark.skipif(
    not os.getenv("PINECONE_API_KEY"),
    reason="Requires PINECONE_API_KEY"
)

def test_list_indexes_integration(client: Client):
    """List indexes against real API."""
    indexes = client.list_indexes()
    assert isinstance(indexes, list)

def test_create_and_delete_index_integration(client: Client):
    """Create and delete index against real API."""
    index_name = f"test-index-{int(time.time())}"

    # Create
    index = client.create_index(
        name=index_name,
        dimension=1536,
        metric="cosine"
    )
    assert index.name == index_name

    # Verify exists
    indexes = client.list_indexes()
    assert index_name in indexes

    # Delete
    client.delete_index(index_name)

    # Verify deleted
    indexes = client.list_indexes()
    assert index_name not in indexes
```

### Example Tests

**Purpose**: Verify example code works

```python
# tests/examples/test_quickstart.py
def test_quickstart_example():
    """Quickstart example code works."""
    # Import and run example
    from examples import quickstart

    result = quickstart.main()
    assert result is not None
```

## Test Patterns

### Fixtures

**Use pytest fixtures for setup**:

```python
# tests/conftest.py
import pytest
from pinecone import Pinecone

@pytest.fixture
def client():
    """Provide a test client."""
    return Client(api_key="test-key")

@pytest.fixture
def mock_http(monkeypatch):
    """Mock HTTP client."""
    mock = Mock()
    monkeypatch.setattr(
        "pinecone._internal.http.client.HTTPClient",
        lambda *args, **kwargs: mock
    )
    return mock

# Usage
def test_with_fixtures(client, mock_http):
    mock_http.get.return_value = httpx.Response(200, json={"indexes": []})
    indexes = client.list_indexes()
    assert indexes == []
```

### Parametrize

**Use parametrize for multiple test cases**:

```python
# ✅ Good: Parametrized test
@pytest.mark.parametrize("status_code,exception", [
    (400, BadRequestError),
    (401, UnauthorizedError),
    (404, NotFoundError),
    (429, RateLimitError),
    (500, ServerError),
])
def test_http_errors(status_code, exception):
    """Test error handling for different status codes."""
    with respx.mock:
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(status_code)
        )

        client = HTTPClient(api_key="test")
        with pytest.raises(exception):
            client.get("/test")

# ❌ Bad: Separate test per case
def test_http_error_400(): ...
def test_http_error_401(): ...
def test_http_error_404(): ...
# Lots of duplication
```

### Mocking

**Mock external dependencies**:

```python
from unittest.mock import Mock, patch

# ✅ Good: Mock HTTP layer
def test_list_indexes_with_mock():
    """Test list_indexes with mocked HTTP."""
    mock_http = Mock()
    mock_http.get.return_value = httpx.Response(
        200,
        json={"indexes": ["index1", "index2"]}
    )

    pc = Pinecone(api_key="test")
    pc._http = mock_http

    indexes = pc.list_indexes()
    assert indexes == ["index1", "index2"]
    mock_http.get.assert_called_once_with("/indexes")

# ❌ Bad: Not mocking external calls
def test_list_indexes():
    """Test list_indexes without mock."""
    pc = Pinecone(api_key="test")
    indexes = pc.list_indexes()  # Real API call!
    assert isinstance(indexes, list)
```

### Async Tests

**Use pytest-asyncio for async tests**:

```python
import pytest

# ✅ Good: Async test with pytest-asyncio
@pytest.mark.asyncio
async def test_async_list_indexes():
    """Test async list_indexes."""
    async with AsyncClient(api_key="test") as client:
        mock_http = AsyncMock()
        mock_http.get.return_value = httpx.Response(
            200,
            json={"indexes": []}
        )
        client._http = mock_http

        indexes = await client.list_indexes()
        assert indexes == []

# ❌ Bad: Missing pytest.mark.asyncio
async def test_async_list_indexes():  # Won't run!
    ...
```

## Test Naming

### Test Function Names

**Pattern**: `test_<component>_<behavior>_<condition>`

```python
# ✅ Good: Descriptive test names
def test_http_client_retries_on_timeout():
    ...

def test_client_raises_invalid_api_key_when_empty():
    ...

def test_rate_limiter_waits_when_tokens_exhausted():
    ...

# ❌ Bad: Vague test names
def test_http():
    ...

def test_client():
    ...

def test_retry():
    ...
```

### Test Docstrings

**Include docstring for complex tests**:

```python
# ✅ Good: Docstring explains test
def test_http_client_exponential_backoff():
    """
    HTTPClient should use exponential backoff for retries.

    First retry waits 1s, second waits 2s, third waits 4s.
    """
    ...

# ❌ Bad: No docstring
def test_http_client_exponential_backoff():
    ...  # What is being tested?
```

## Assertions

### Specific Assertions

**Use specific assertions**:

```python
# ✅ Good: Specific assertions
def test_create_index():
    index = client.create_index("test", 1536)

    assert index.name == "test"
    assert index.dimension == 1536
    assert index.metric == "cosine"

# ❌ Bad: Vague assertions
def test_create_index():
    index = client.create_index("test", 1536)

    assert index  # What about it?
    assert index.name  # What should it be?
```

### Exception Testing

**Test exceptions properly**:

```python
# ✅ Good: Test exception details
def test_invalid_api_key_error():
    with pytest.raises(InvalidAPIKeyError) as exc_info:
        pc = Pinecone(api_key="")

    assert "API key is required" in str(exc_info.value)

# ❌ Bad: Only check exception raised
def test_invalid_api_key_error():
    with pytest.raises(Exception):  # Too broad
        pc = Pinecone(api_key="")
```

### Type Checking

**Verify types in tests**:

```python
# ✅ Good: Check types
def test_list_indexes_returns_list():
    indexes = client.list_indexes()

    assert isinstance(indexes, list)
    assert all(isinstance(name, str) for name in indexes)

# ❌ Bad: No type checking
def test_list_indexes():
    indexes = client.list_indexes()

    assert indexes  # Could be any truthy value
```

## Coverage

### Target Coverage

**Aim for high coverage**:

- Specification tests: 100% (all protocols checked)
- Unit tests: 90%+ coverage
- Integration tests: Critical paths only

### Run Coverage

```bash
# Run with coverage
pytest --cov=pinecone --cov-report=html

# View coverage report
open htmlcov/index.html
```

### Coverage Configuration

```toml
# pyproject.toml
[tool.coverage.run]
source = ["pinecone"]
omit = [
    "*/tests/*",
    "*/__pycache__/*",
    "*/site-packages/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]
```

## Performance Testing

### Benchmark Tests

**Mark performance-critical tests**:

```python
import pytest

@pytest.mark.benchmark
def test_msgspec_performance(benchmark):
    """msgspec should be faster than json."""
    data = {"vectors": [{"id": f"vec{i}", "values": [0.1] * 1536}
                        for i in range(1000)]}

    def parse_msgspec():
        return msgspec.json.decode(msgspec.json.encode(data))

    result = benchmark(parse_msgspec)
    assert result is not None

# Run benchmarks
pytest -m benchmark --benchmark-only
```

### Load Tests

**Test rate limiting and connection pooling**:

```python
@pytest.mark.load
async def test_concurrent_requests():
    """Client should handle concurrent requests."""
    async with AsyncClient(api_key="test") as client:
        # 100 concurrent requests
        tasks = [client.list_indexes() for _ in range(100)]
        results = await asyncio.gather(*tasks)

        assert len(results) == 100
        assert all(isinstance(r, list) for r in results)
```

## Test Configuration

### pytest.ini

```ini
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto

markers =
    unit: Unit tests (fast, isolated)
    integration: Integration tests (require API key)
    benchmark: Performance benchmarks
    load: Load tests
    slow: Slow-running tests

# Fail on warnings
filterwarnings =
    error
    ignore::DeprecationWarning
```

### conftest.py

```python
# tests/conftest.py
import pytest
import os

def pytest_configure(config):
    """Configure pytest."""
    config.addinivalue_line("markers", "unit: Unit tests")
    config.addinivalue_line("markers", "integration: Integration tests")

@pytest.fixture(scope="session")
def api_key():
    """Get API key from environment."""
    key = os.getenv("PINECONE_API_KEY")
    if not key:
        pytest.skip("PINECONE_API_KEY not set")
    return key

@pytest.fixture
def client(api_key):
    """Provide test client."""
    return Client(api_key=api_key)
```

## Anti-Patterns

### ❌ Testing Implementation Details

```python
# Bad: Testing private methods
def test_parse_response():
    result = client._parse_response(response)  # Internal detail
    assert result == expected
```

**Solution**: Test public interface.

```python
# Good: Testing public behavior
def test_list_indexes():
    indexes = client.list_indexes()  # Public method
    assert isinstance(indexes, list)
```

### ❌ Slow Unit Tests

```python
# Bad: Unit test with real HTTP
def test_create_index():
    pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
    index = pc.create_index("test", 1536)  # Real API call
    assert index.name == "test"
```

**Solution**: Mock HTTP layer.

```python
# Good: Fast unit test with mock
@respx.mock
def test_create_index():
    respx.post("https://api.pinecone.io/indexes").mock(
        return_value=httpx.Response(200, json={"name": "test"})
    )
    pc = Pinecone(api_key="test")
    index = pc.create_index("test", 1536)
    assert index.name == "test"
```

### ❌ Brittle Tests

```python
# Bad: Testing exact implementation
def test_list_indexes():
    indexes = client.list_indexes()
    assert len(indexes) == 3  # Breaks if data changes
    assert indexes[0] == "index1"  # Order matters
```

**Solution**: Test invariants, not specifics.

```python
# Good: Testing behavior
def test_list_indexes():
    indexes = client.list_indexes()
    assert isinstance(indexes, list)
    assert all(isinstance(name, str) for name in indexes)
```

### ❌ Shared Test State

```python
# Bad: Tests depend on each other
def test_create_index():
    global test_index
    test_index = client.create_index("test", 1536)

def test_delete_index():
    client.delete_index(test_index.name)  # Depends on previous test
```

**Solution**: Independent tests with fixtures.

```python
# Good: Independent tests
@pytest.fixture
def test_index(client):
    index = client.create_index(f"test-{uuid.uuid4()}", 1536)
    yield index
    client.delete_index(index.name)

def test_delete_index(client, test_index):
    client.delete_index(test_index.name)
    assert test_index.name not in client.list_indexes()
```

## Running Tests

### Run All Tests

```bash
pytest
```

### Run Specific Test Types

```bash
# Unit tests only (fast)
pytest -m unit

# Integration tests (requires API key)
pytest -m integration

# Specific file
pytest tests/unit/http/test_client.py

# Specific test
pytest tests/unit/http/test_client.py::test_http_client_get
```

### Run with Options

```bash
# Verbose output
pytest -v

# Stop on first failure
pytest -x

# Run in parallel (requires pytest-xdist)
pytest -n auto

# With coverage
pytest --cov=pinecone --cov-report=html
```

## References

- See [architecture.md](./architecture.md) for component boundaries
- See [errors.md](./errors.md) for exception testing patterns
- See [async.md](./async.md) for async testing patterns
- See [types.md](./types.md) for type checking in tests
