# Testing Guide

## Overview

The Wizamart platform employs a comprehensive testing strategy with four distinct test levels: **Unit**, **Integration**, **System**, and **Performance** tests. This guide explains how to write, run, and maintain tests in the application.

## Table of Contents

- [Test Architecture](#test-architecture)
- [Running Tests](#running-tests)
- [Test Structure](#test-structure)
- [Writing Tests](#writing-tests)
- [Fixtures](#fixtures)
- [Mocking](#mocking)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)

---

## Test Architecture

### Test Pyramid

Our testing strategy follows the test pyramid approach:

```
           /\
          /  \
         / P  \    Performance Tests (Slow, End-to-End)
        /______\
       /        \
      /  System  \  System Tests (End-to-End Scenarios)
     /____________\
    /              \
   /  Integration   \ Integration Tests (Multiple Components)
  /__________________\
 /                    \
/        Unit          \ Unit Tests (Individual Components)
/______________________\
```

### Test Levels

#### 1. Unit Tests (`tests/unit/`)
- **Purpose**: Test individual components in isolation
- **Scope**: Single functions, methods, or classes
- **Dependencies**: Mocked or stubbed
- **Speed**: Very fast (< 100ms per test)
- **Marker**: `@pytest.mark.unit`

**Example Structure:**
```
tests/unit/
├── conftest.py              # Unit test specific fixtures
├── services/                # Service layer tests
│   ├── test_auth_service.py
│   ├── test_vendor_service.py
│   └── test_product_service.py
├── middleware/              # Middleware tests
│   ├── test_auth.py
│   ├── test_context.py
│   └── test_rate_limiter.py
├── models/                  # Model tests
│   └── test_database_models.py
└── utils/                   # Utility function tests
    ├── test_csv_processor.py
    └── test_data_validation.py
```

#### 2. Integration Tests (`tests/integration/`)
- **Purpose**: Test multiple components working together
- **Scope**: API endpoints, database interactions, middleware flows
- **Dependencies**: Real database (in-memory SQLite), test client
- **Speed**: Fast to moderate (100ms - 1s per test)
- **Marker**: `@pytest.mark.integration`

**Example Structure:**
```
tests/integration/
├── conftest.py              # Integration test specific fixtures
├── api/                     # API endpoint tests
│   └── v1/
│       ├── test_auth_endpoints.py
│       ├── test_vendor_endpoints.py
│       ├── test_pagination.py
│       └── test_filtering.py
├── middleware/              # Middleware stack tests
│   ├── test_middleware_stack.py
│   └── test_context_detection_flow.py
├── security/                # Security integration tests
│   ├── test_authentication.py
│   ├── test_authorization.py
│   └── test_input_validation.py
└── workflows/               # Multi-step workflow tests
    └── test_integration.py
```

#### 3. System Tests (`tests/system/`)
- **Purpose**: Test complete application behavior
- **Scope**: End-to-end user scenarios
- **Dependencies**: Full application stack
- **Speed**: Moderate to slow (1s - 5s per test)
- **Marker**: `@pytest.mark.system`

**Example Structure:**
```
tests/system/
├── conftest.py              # System test specific fixtures
└── test_error_handling.py   # System-wide error handling tests
```

#### 4. Performance Tests (`tests/performance/`)
- **Purpose**: Test application performance and load handling
- **Scope**: Response times, concurrent requests, large datasets
- **Dependencies**: Full application stack with data
- **Speed**: Slow (5s+ per test)
- **Markers**: `@pytest.mark.performance` and `@pytest.mark.slow`

**Example Structure:**
```
tests/performance/
├── conftest.py              # Performance test specific fixtures
└── test_api_performance.py  # API performance tests
```

---

## Running Tests

### Using Make Commands

The project provides convenient Make commands for running tests:

```bash
# Run all tests
make test

# Run specific test levels
make test-unit          # Unit tests only
make test-integration   # Integration tests only

# Run with coverage report
make test-coverage      # Generates HTML coverage report in htmlcov/

# Run fast tests (excludes @pytest.mark.slow)
make test-fast

# Run slow tests only
make test-slow
```

### Using pytest Directly

```bash
# Run all tests
pytest tests/

# Run specific test level by marker
pytest tests/ -m unit
pytest tests/ -m integration
pytest tests/ -m "system or performance"

# Run specific test file
pytest tests/unit/services/test_auth_service.py

# Run specific test class
pytest tests/unit/services/test_auth_service.py::TestAuthService

# Run specific test method
pytest tests/unit/services/test_auth_service.py::TestAuthService::test_register_user_success

# Run with verbose output
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=app --cov=models --cov=middleware

# Run tests matching a pattern
pytest tests/ -k "auth"  # Runs all tests with "auth" in the name
```

### Using Test Markers

Markers allow you to categorize and selectively run tests:

```bash
# Run by functionality marker
pytest -m auth          # Authentication tests
pytest -m products      # Product tests
pytest -m vendors       # Vendor tests
pytest -m api           # API endpoint tests

# Run by test type marker
pytest -m unit          # Unit tests
pytest -m integration   # Integration tests
pytest -m system        # System tests
pytest -m performance   # Performance tests

# Run by speed marker
pytest -m "not slow"    # Exclude slow tests
pytest -m slow          # Run only slow tests

# Combine markers
pytest -m "unit and auth"           # Unit tests for auth
pytest -m "integration and api"     # Integration tests for API
pytest -m "not (slow or external)"  # Exclude slow and external tests
```

### Test Markers Reference

All available markers are defined in `pytest.ini`:

| Marker | Description |
|--------|-------------|
| `unit` | Fast, isolated component tests |
| `integration` | Multiple components working together |
| `system` | Full application behavior tests |
| `e2e` | End-to-end user workflow tests |
| `slow` | Tests that take significant time (>1s) |
| `performance` | Performance and load tests |
| `auth` | Authentication and authorization tests |
| `products` | Product management functionality |
| `inventory` | Inventory management tests |
| `vendors` | Vendor management functionality |
| `admin` | Admin functionality and permissions |
| `marketplace` | Marketplace import functionality |
| `stats` | Statistics and reporting |
| `database` | Tests requiring database operations |
| `external` | Tests requiring external services |
| `api` | API endpoint tests |
| `security` | Security-related tests |
| `ci` | Tests that should only run in CI |
| `dev` | Development-specific tests |

---

## Test Structure

### Test Organization

Tests are organized by:
1. **Test level** (unit/integration/system/performance)
2. **Application layer** (services/middleware/models/utils)
3. **Functionality** (auth/vendors/products/inventory)

### File Naming Conventions

- Test files: `test_*.py`
- Test classes: `Test*`
- Test functions: `test_*`
- Fixture files: `*_fixtures.py`
- Configuration files: `conftest.py`

### Test Class Structure

```python
import pytest
from app.services.auth_service import AuthService
from app.exceptions import InvalidCredentialsException

@pytest.mark.unit
@pytest.mark.auth
class TestAuthService:
    """Test suite for AuthService."""

    def setup_method(self):
        """Setup method runs before each test."""
        self.service = AuthService()

    def test_successful_login(self, db, test_user):
        """Test successful user login."""
        # Arrange
        username = test_user.username
        password = "testpass123"

        # Act
        result = self.service.login(db, username, password)

        # Assert
        assert result is not None
        assert result.access_token is not None

    def test_invalid_credentials(self, db, test_user):
        """Test login with invalid credentials."""
        # Act & Assert
        with pytest.raises(InvalidCredentialsException):
            self.service.login(db, test_user.username, "wrongpassword")
```

### Test Naming Conventions

Use descriptive test names that explain:
- What is being tested
- Under what conditions
- What the expected outcome is

**Good examples:**
```python
def test_create_vendor_success(self, db, test_user):
    """Test successful vendor creation by regular user."""

def test_create_vendor_duplicate_code_raises_exception(self, db, test_user):
    """Test vendor creation fails when vendor code already exists."""

def test_admin_can_delete_any_vendor(self, db, test_admin, test_vendor):
    """Test admin has permission to delete any vendor."""
```

**Poor examples:**
```python
def test_vendor(self):  # Too vague
def test_1(self):       # Non-descriptive
def test_error(self):   # Unclear what error
```

---

## Writing Tests

### Unit Test Example

Unit tests focus on testing a single component in isolation:

```python
# tests/unit/services/test_auth_service.py
import pytest
from app.services.auth_service import AuthService
from app.exceptions import UserAlreadyExistsException
from models.schema.auth import UserRegister

@pytest.mark.unit
@pytest.mark.auth
class TestAuthService:
    """Test suite for AuthService."""

    def setup_method(self):
        """Initialize service instance."""
        self.service = AuthService()

    def test_register_user_success(self, db):
        """Test successful user registration."""
        # Arrange
        user_data = UserRegister(
            email="newuser@example.com",
            username="newuser123",
            password="securepass123"
        )

        # Act
        user = self.service.register_user(db, user_data)

        # Assert
        assert user is not None
        assert user.email == "newuser@example.com"
        assert user.username == "newuser123"
        assert user.role == "user"
        assert user.is_active is True
        assert user.hashed_password != "securepass123"

    def test_register_user_duplicate_email(self, db, test_user):
        """Test registration fails when email already exists."""
        # Arrange
        user_data = UserRegister(
            email=test_user.email,
            username="differentuser",
            password="securepass123"
        )

        # Act & Assert
        with pytest.raises(UserAlreadyExistsException) as exc_info:
            self.service.register_user(db, user_data)

        # Verify exception details
        exception = exc_info.value
        assert exception.error_code == "USER_ALREADY_EXISTS"
        assert exception.status_code == 409
```

### Integration Test Example

Integration tests verify multiple components work together:

```python
# tests/integration/api/v1/test_auth_endpoints.py
import pytest

@pytest.mark.integration
@pytest.mark.api
@pytest.mark.auth
class TestAuthenticationAPI:
    """Integration tests for authentication API endpoints."""

    def test_register_user_success(self, client, db):
        """Test successful user registration via API."""
        # Act
        response = client.post(
            "/api/v1/auth/register",
            json={
                "email": "newuser@example.com",
                "username": "newuser",
                "password": "securepass123"
            }
        )

        # Assert
        assert response.status_code == 200
        data = response.json()
        assert data["email"] == "newuser@example.com"
        assert data["username"] == "newuser"
        assert data["role"] == "user"
        assert "hashed_password" not in data

    def test_login_returns_access_token(self, client, test_user):
        """Test login endpoint returns valid access token."""
        # Act
        response = client.post(
            "/api/v1/auth/login",
            json={
                "username": test_user.username,
                "password": "testpass123"
            }
        )

        # Assert
        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert data["token_type"] == "bearer"

    def test_protected_endpoint_requires_authentication(self, client):
        """Test protected endpoint returns 401 without token."""
        # Act
        response = client.get("/api/v1/vendor")

        # Assert
        assert response.status_code == 401
```

### System Test Example

System tests verify complete end-to-end scenarios:

```python
# tests/system/test_error_handling.py
import pytest

@pytest.mark.system
class TestErrorHandling:
    """System tests for error handling across the API."""

    def test_invalid_json_request(self, client, auth_headers):
        """Test handling of malformed JSON requests."""
        # Act
        response = client.post(
            "/api/v1/vendor",
            headers=auth_headers,
            content="{ invalid json syntax"
        )

        # Assert
        assert response.status_code == 422
        data = response.json()
        assert data["error_code"] == "VALIDATION_ERROR"
        assert data["message"] == "Request validation failed"
        assert "validation_errors" in data["details"]
```

### Performance Test Example

Performance tests measure response times and system capacity:

```python
# tests/performance/test_api_performance.py
import time
import pytest
from models.database.marketplace_product import MarketplaceProduct

@pytest.mark.performance
@pytest.mark.slow
@pytest.mark.database
class TestPerformance:
    """Performance tests for API endpoints."""

    def test_product_list_performance(self, client, auth_headers, db):
        """Test performance of product listing with many products."""
        # Arrange - Create test data
        products = []
        for i in range(100):
            product = MarketplaceProduct(
                marketplace_product_id=f"PERF{i:03d}",
                title=f"Performance Test Product {i}",
                price=f"{i}.99",
                marketplace="Performance"
            )
            products.append(product)

        db.add_all(products)
        db.commit()

        # Act - Time the request
        start_time = time.time()
        response = client.get(
            "/api/v1/marketplace/product?limit=100",
            headers=auth_headers
        )
        end_time = time.time()

        # Assert
        assert response.status_code == 200
        assert len(response.json()["products"]) == 100
        assert end_time - start_time < 2.0  # Must complete in 2s
```

---

## Fixtures

### Fixture Architecture

Fixtures are organized in a hierarchical structure:

```
tests/
├── conftest.py                      # Core fixtures (session-scoped)
│   ├── engine                       # Database engine
│   ├── testing_session_local        # Session factory
│   ├── db                           # Database session (function-scoped)
│   └── client                       # Test client
│
├── fixtures/                        # Reusable fixture modules
│   ├── testing_fixtures.py          # Testing utilities
│   ├── auth_fixtures.py             # Auth-related fixtures
│   ├── vendor_fixtures.py           # Vendor fixtures
│   ├── marketplace_product_fixtures.py
│   ├── marketplace_import_job_fixtures.py
│   └── customer_fixtures.py
│
├── unit/conftest.py                 # Unit test specific fixtures
├── integration/conftest.py          # Integration test specific fixtures
├── system/conftest.py               # System test specific fixtures
└── performance/conftest.py          # Performance test specific fixtures
```

### Core Fixtures (tests/conftest.py)

These fixtures are available to all tests:

```python
# Database fixtures
@pytest.fixture(scope="session")
def engine():
    """Create test database engine (in-memory SQLite)."""
    return create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
        echo=False
    )

@pytest.fixture(scope="function")
def db(engine, testing_session_local):
    """Create a clean database session for each test."""
    Base.metadata.create_all(bind=engine)
    db_session = testing_session_local()

    try:
        yield db_session
    finally:
        db_session.close()
        Base.metadata.drop_all(bind=engine)
        Base.metadata.create_all(bind=engine)

@pytest.fixture(scope="function")
def client(db):
    """Create a test client with database dependency override."""
    def override_get_db():
        try:
            yield db
        finally:
            pass

    app.dependency_overrides[get_db] = override_get_db

    try:
        client = TestClient(app)
        yield client
    finally:
        if get_db in app.dependency_overrides:
            del app.dependency_overrides[get_db]
```

### Auth Fixtures (tests/fixtures/auth_fixtures.py)

Authentication and user-related fixtures:

```python
@pytest.fixture
def test_user(db, auth_manager):
    """Create a test user with unique username."""
    unique_id = str(uuid.uuid4())[:8]
    hashed_password = auth_manager.hash_password("testpass123")
    user = User(
        email=f"test_{unique_id}@example.com",
        username=f"testuser_{unique_id}",
        hashed_password=hashed_password,
        role="user",
        is_active=True
    )
    db.add(user)
    db.commit()
    db.refresh(user)
    db.expunge(user)  # Prevent resource warnings
    return user

@pytest.fixture
def test_admin(db, auth_manager):
    """Create a test admin user."""
    unique_id = str(uuid.uuid4())[:8]
    hashed_password = auth_manager.hash_password("adminpass123")
    admin = User(
        email=f"admin_{unique_id}@example.com",
        username=f"admin_{unique_id}",
        hashed_password=hashed_password,
        role="admin",
        is_active=True
    )
    db.add(admin)
    db.commit()
    db.refresh(admin)
    db.expunge(admin)
    return admin

@pytest.fixture
def auth_headers(client, test_user):
    """Get authentication headers for test user."""
    response = client.post(
        "/api/v1/auth/login",
        json={"username": test_user.username, "password": "testpass123"}
    )
    assert response.status_code == 200
    token = response.json()["access_token"]
    return {"Authorization": f"Bearer {token}"}

@pytest.fixture
def admin_headers(client, test_admin):
    """Get authentication headers for admin user."""
    response = client.post(
        "/api/v1/auth/login",
        json={"username": test_admin.username, "password": "adminpass123"}
    )
    assert response.status_code == 200
    token = response.json()["access_token"]
    return {"Authorization": f"Bearer {token}"}
```

### Vendor Fixtures (tests/fixtures/vendor_fixtures.py)

Vendor-related fixtures:

```python
@pytest.fixture
def test_vendor(db, test_user):
    """Create a test vendor."""
    unique_id = str(uuid.uuid4())[:8].upper()
    vendor = Vendor(
        vendor_code=f"TESTVENDOR_{unique_id}",
        subdomain=f"testvendor{unique_id.lower()}",
        name=f"Test Vendor {unique_id.lower()}",
        owner_user_id=test_user.id,
        is_active=True,
        is_verified=True
    )
    db.add(vendor)
    db.commit()
    db.refresh(vendor)
    db.expunge(vendor)
    return vendor

@pytest.fixture
def vendor_factory():
    """Factory function to create unique vendors."""
    return create_unique_vendor_factory()

def create_unique_vendor_factory():
    """Factory function to create unique vendors in tests."""
    def _create_vendor(db, owner_user_id, **kwargs):
        unique_id = str(uuid.uuid4())[:8]
        defaults = {
            "vendor_code": f"FACTORY_{unique_id.upper()}",
            "subdomain": f"factory{unique_id.lower()}",
            "name": f"Factory Vendor {unique_id}",
            "owner_user_id": owner_user_id,
            "is_active": True,
            "is_verified": False
        }
        defaults.update(kwargs)

        vendor = Vendor(**defaults)
        db.add(vendor)
        db.commit()
        db.refresh(vendor)
        return vendor

    return _create_vendor
```

### Using Fixtures

Fixtures are automatically injected by pytest:

```python
def test_example(db, test_user, auth_headers):
    """
    This test receives three fixtures:
    - db: Database session
    - test_user: A test user object
    - auth_headers: Authentication headers for API calls
    """
    # Use the fixtures
    assert test_user.id is not None
    response = client.get("/api/v1/profile", headers=auth_headers)
    assert response.status_code == 200
```

### Creating Custom Fixtures

Add fixtures to appropriate conftest.py or fixture files:

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

@pytest.fixture
def mock_service():
    """Create a mocked service for unit testing."""
    from unittest.mock import Mock
    service = Mock()
    service.get_data.return_value = {"key": "value"}
    return service
```

---

## Mocking

### When to Use Mocks

Use mocks to:
- Isolate unit tests from external dependencies
- Simulate error conditions
- Test edge cases
- Speed up tests by avoiding slow operations

### Mock Patterns

#### 1. Mocking with unittest.mock

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

@pytest.mark.unit
class TestWithMocks:
    def test_with_mock(self):
        """Test using a simple mock."""
        mock_service = Mock()
        mock_service.get_user.return_value = {"id": 1, "name": "Test"}

        result = mock_service.get_user(1)

        assert result["name"] == "Test"
        mock_service.get_user.assert_called_once_with(1)

    @patch('app.services.auth_service.jwt.encode')
    def test_with_patch(self, mock_encode):
        """Test using patch decorator."""
        mock_encode.return_value = "fake_token"

        service = AuthService()
        token = service.create_token({"user_id": 1})

        assert token == "fake_token"
        mock_encode.assert_called_once()
```

#### 2. Mocking Database Errors

```python
from sqlalchemy.exc import SQLAlchemyError

def test_database_error_handling():
    """Test handling of database errors."""
    mock_db = Mock()
    mock_db.commit.side_effect = SQLAlchemyError("DB connection failed")

    service = VendorService()

    with pytest.raises(DatabaseException):
        service.create_vendor(mock_db, vendor_data, user)
```

#### 3. Mocking External Services

```python
@patch('app.services.email_service.send_email')
def test_user_registration_sends_email(mock_send_email, db):
    """Test that user registration sends a welcome email."""
    service = AuthService()
    user_data = UserRegister(
        email="test@example.com",
        username="testuser",
        password="password123"
    )

    service.register_user(db, user_data)

    # Verify email was sent
    mock_send_email.assert_called_once()
    call_args = mock_send_email.call_args
    assert "test@example.com" in str(call_args)
```

#### 4. Mocking with Context Managers

```python
@pytest.mark.unit
class TestFileOperations:
    @patch('builtins.open', create=True)
    def test_file_read(self, mock_open):
        """Test file reading operation."""
        mock_open.return_value.__enter__.return_value.read.return_value = "test data"

        # Code that opens and reads a file
        with open('test.txt', 'r') as f:
            data = f.read()

        assert data == "test data"
        mock_open.assert_called_once_with('test.txt', 'r')
```

### Fixtures for Error Testing

Use fixtures to create consistent mock errors:

```python
# tests/fixtures/testing_fixtures.py
@pytest.fixture
def db_with_error():
    """Database session that raises errors."""
    mock_db = Mock()
    mock_db.query.side_effect = SQLAlchemyError("Database connection failed")
    mock_db.add.side_effect = SQLAlchemyError("Database insert failed")
    mock_db.commit.side_effect = SQLAlchemyError("Database commit failed")
    mock_db.rollback.return_value = None
    return mock_db

# Usage in tests
def test_handles_db_error(db_with_error):
    """Test graceful handling of database errors."""
    service = VendorService()

    with pytest.raises(DatabaseException):
        service.create_vendor(db_with_error, vendor_data, user)
```

---

## Best Practices

### 1. Test Independence

Each test should be completely independent:

```python
# ✅ GOOD - Independent tests
def test_create_user(db):
    user = create_user(db, "user1@test.com")
    assert user.email == "user1@test.com"

def test_delete_user(db):
    user = create_user(db, "user2@test.com")  # Creates own data
    delete_user(db, user.id)
    assert get_user(db, user.id) is None

# ❌ BAD - Tests depend on each other
user_id = None

def test_create_user(db):
    global user_id
    user = create_user(db, "user@test.com")
    user_id = user.id  # State shared between tests

def test_delete_user(db):
    delete_user(db, user_id)  # Depends on previous test
```

### 2. SQLAlchemy Fixture Best Practices

**IMPORTANT**: Follow these rules when working with SQLAlchemy fixtures to avoid common pitfalls:

#### ❌ Never Use `db.expunge()` in Fixtures

```python
# ❌ BAD - Causes lazy loading errors
@pytest.fixture
def test_user(db, auth_manager):
    user = User(email="test@example.com", ...)
    db.add(user)
    db.commit()
    db.refresh(user)
    db.expunge(user)  # ❌ NEVER DO THIS!
    return user

# When you later try to access relationships:
# user.company.name  # ❌ DetachedInstanceError!
```

```python
# ✅ GOOD - Keep objects attached to session
@pytest.fixture
def test_user(db, auth_manager):
    user = User(email="test@example.com", ...)
    db.add(user)
    db.commit()
    db.refresh(user)
    return user  # ✅ Object stays attached to session
```

#### Why `db.expunge()` Is an Anti-Pattern

1. **Breaks lazy loading**: Detached objects cannot access relationships like `user.company` or `product.marketplace_product`
2. **Causes DetachedInstanceError**: SQLAlchemy throws errors when accessing unloaded relationships on detached objects
3. **Test isolation is already handled**: The `db` fixture drops and recreates all tables after each test

#### How Test Isolation Works

The `db` fixture in `tests/conftest.py` provides isolation by:
- Creating fresh tables before each test
- Dropping all tables after each test completes
- Using `expire_on_commit=False` to prevent objects from expiring after commits

```python
@pytest.fixture(scope="function")
def db(engine, testing_session_local):
    Base.metadata.create_all(bind=engine)  # Fresh tables
    db_session = testing_session_local()

    try:
        yield db_session
    finally:
        db_session.close()
        Base.metadata.drop_all(bind=engine)    # Clean up
        Base.metadata.create_all(bind=engine)  # Ready for next test
```

#### If You Need Fresh Data

Use `db.refresh(obj)` instead of expunge/re-query patterns:

```python
# ✅ GOOD - Refresh for fresh data
def test_update_user(db, test_user):
    update_user_email(db, test_user.id, "new@example.com")
    db.refresh(test_user)  # Get latest data from database
    assert test_user.email == "new@example.com"
```

### 3. Arrange-Act-Assert Pattern

Structure tests clearly using AAA pattern:

```python
def test_vendor_creation(db, test_user):
    # Arrange - Set up test data
    vendor_data = VendorCreate(
        vendor_code="TESTVENDOR",
        vendor_name="Test Vendor"
    )

    # Act - Perform the action
    vendor = VendorService().create_vendor(db, vendor_data, test_user)

    # Assert - Verify the results
    assert vendor.vendor_code == "TESTVENDOR"
    assert vendor.owner_user_id == test_user.id
```

### 3. Test One Thing

Each test should verify a single behavior:

```python
# ✅ GOOD - Tests one specific behavior
def test_admin_creates_verified_vendor(db, test_admin):
    """Test that admin users create verified vendors."""
    vendor = create_vendor(db, test_admin)
    assert vendor.is_verified is True

def test_regular_user_creates_unverified_vendor(db, test_user):
    """Test that regular users create unverified vendors."""
    vendor = create_vendor(db, test_user)
    assert vendor.is_verified is False

# ❌ BAD - Tests multiple behaviors
def test_vendor_creation(db, test_admin, test_user):
    """Test vendor creation."""  # Vague docstring
    admin_vendor = create_vendor(db, test_admin)
    user_vendor = create_vendor(db, test_user)

    assert admin_vendor.is_verified is True
    assert user_vendor.is_verified is False
    assert admin_vendor.is_active is True
    assert user_vendor.is_active is True
    # Testing too many things
```

### 4. Use Descriptive Names

Test names should describe what is being tested:

```python
# ✅ GOOD
def test_create_vendor_with_duplicate_code_raises_exception(db, test_vendor):
    """Test that creating vendor with duplicate code raises VendorAlreadyExistsException."""

# ❌ BAD
def test_vendor_error(db):
    """Test vendor."""
```

### 5. Test Error Cases

Always test both success and failure paths:

```python
class TestVendorService:
    def test_create_vendor_success(self, db, test_user):
        """Test successful vendor creation."""
        # Test happy path

    def test_create_vendor_duplicate_code(self, db, test_user, test_vendor):
        """Test error when vendor code already exists."""
        # Test error case

    def test_create_vendor_invalid_data(self, db, test_user):
        """Test error with invalid vendor data."""
        # Test validation error

    def test_create_vendor_unauthorized(self, db):
        """Test error when user is not authorized."""
        # Test authorization error
```

### 6. Use Markers Consistently

Apply appropriate markers to all tests:

```python
@pytest.mark.unit  # Test level
@pytest.mark.auth  # Functionality
class TestAuthService:
    def test_login_success(self):
        """Test successful login."""
```

### 7. Clean Up Resources

Ensure proper cleanup to prevent resource leaks:

```python
@pytest.fixture
def test_user(db, auth_manager):
    """Create a test user."""
    user = User(...)
    db.add(user)
    db.commit()
    db.refresh(user)
    db.expunge(user)  # ✅ Detach from session
    return user
```

### 8. Avoid Testing Implementation Details

Test behavior, not implementation:

```python
# ✅ GOOD - Tests behavior
def test_password_is_hashed(db):
    """Test that passwords are stored hashed."""
    user = create_user(db, "test@example.com", "password123")
    assert user.hashed_password != "password123"
    assert len(user.hashed_password) > 50

# ❌ BAD - Tests implementation
def test_password_uses_bcrypt(db):
    """Test that password hashing uses bcrypt."""
    user = create_user(db, "test@example.com", "password123")
    assert user.hashed_password.startswith("$2b$")  # Assumes bcrypt
```

### 9. Test Data Uniqueness

Use UUIDs to ensure test data uniqueness:

```python
def test_concurrent_user_creation(db):
    """Test creating multiple users with unique data."""
    unique_id = str(uuid.uuid4())[:8]
    user = User(
        email=f"test_{unique_id}@example.com",
        username=f"testuser_{unique_id}"
    )
    db.add(user)
    db.commit()
```

### 10. Coverage Goals

Aim for high test coverage:
- Unit tests: > 90% coverage
- Integration tests: > 80% coverage
- Overall: > 80% coverage (enforced in pytest.ini)

Check coverage with:
```bash
make test-coverage
# Opens htmlcov/index.html in browser
```

---

## Troubleshooting

### Common Issues

#### Issue: Tests Fail Intermittently

**Cause**: Tests depend on each other or share state

**Solution**: Ensure test independence, use function-scoped fixtures

```python
# Use function scope for fixtures that modify data
@pytest.fixture(scope="function")  # ✅ New instance per test
def db(engine, testing_session_local):
    ...
```

#### Issue: "ResourceWarning: unclosed connection"

**Cause**: Database sessions not properly closed

**Solution**: Use `db.expunge()` after creating test objects

```python
@pytest.fixture
def test_user(db):
    user = User(...)
    db.add(user)
    db.commit()
    db.refresh(user)
    db.expunge(user)  # ✅ Detach from session
    return user
```

#### Issue: Tests Slow

**Cause**: Too many database operations, not using transactions

**Solution**:
- Mark slow tests with `@pytest.mark.slow`
- Use session-scoped fixtures where possible
- Batch database operations

```python
# ✅ Batch create
products = [Product(...) for i in range(100)]
db.add_all(products)
db.commit()

# ❌ Individual creates (slow)
for i in range(100):
    product = Product(...)
    db.add(product)
    db.commit()
```

#### Issue: Import Errors

**Cause**: Python path not set correctly

**Solution**: Run tests from project root:

```bash
# ✅ From project root
pytest tests/

# ❌ From tests directory
cd tests && pytest .
```

#### Issue: Fixture Not Found

**Cause**: Fixture not imported or registered

**Solution**: Check `pytest_plugins` in conftest.py:

```python
# tests/conftest.py
pytest_plugins = [
    "tests.fixtures.auth_fixtures",
    "tests.fixtures.vendor_fixtures",
    # Add your fixture module here
]
```

### Debug Tips

#### 1. Verbose Output

```bash
pytest tests/ -v  # Verbose test names
pytest tests/ -vv  # Very verbose (shows assertion details)
```

#### 2. Show Local Variables

```bash
pytest tests/ --showlocals  # Show local vars on failure
pytest tests/ -l            # Short form
```

#### 3. Drop to Debugger on Failure

```bash
pytest tests/ --pdb  # Drop to pdb on first failure
```

#### 4. Run Specific Test

```bash
pytest tests/unit/services/test_auth_service.py::TestAuthService::test_login_success -v
```

#### 5. Show Print Statements

```bash
pytest tests/ -s  # Show print() and log output
```

#### 6. Run Last Failed Tests

```bash
pytest tests/ --lf  # Last failed
pytest tests/ --ff  # Failed first
```

### Getting Help

If you encounter issues:

1. Check this documentation
2. Review test examples in the codebase
3. Check pytest.ini for configuration
4. Review conftest.py files for available fixtures
5. Ask team members or create an issue

---

## Summary

This testing guide covers:
- ✅ Four-level test architecture (Unit/Integration/System/Performance)
- ✅ Running tests with Make commands and pytest
- ✅ Test structure and organization
- ✅ Writing tests for each level
- ✅ Using fixtures effectively
- ✅ Mocking patterns and best practices
- ✅ Best practices for maintainable tests
- ✅ Troubleshooting common issues

For information about maintaining and extending the test suite, see [Test Maintenance Guide](test-maintenance.md).
