Files
wehub-resource-sync 5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:44 +08:00

1035 lines
29 KiB
Markdown

# Opik Python SDK Testing Guide
## Table of Contents
- [Overview](#overview)
- [Test Directory Structure](#test-directory-structure)
- [Test Categories](#test-categories)
- [Testing Infrastructure](#testing-infrastructure)
- [Testing Patterns](#testing-patterns)
- [Writing Tests](#writing-tests)
- [Running Tests](#running-tests)
## Overview
The Opik Python SDK has a comprehensive test suite organized into multiple categories:
1. **Unit Tests**: Fast, isolated tests with no external dependencies
2. **Library Integration Tests**: Tests integrations using fake backend
3. **E2E Tests**: Real backend tests for core functionality
4. **E2E Library Integration Tests**: Real backend tests for library integrations
5. **Smoke Tests**: Quick sanity checks
### Testing Philosophy
- **Fast Feedback**: Unit tests run quickly for rapid development
- **Isolation**: Use fake backends to avoid network dependencies
- **Realism**: E2E tests validate against real backend
- **Coverage**: Test both happy paths and edge cases
- **Maintainability**: Shared utilities and clear patterns
## Test Directory Structure
```
tests/
├── conftest.py # Root fixtures (context cleanup, client shutdown)
├── pytest.ini # Pytest configuration
├── test_requirements.txt # Test dependencies
├── testlib/ # Shared testing utilities
│ ├── models.py # Test data models (TraceModel, SpanModel, etc.)
│ ├── backend_emulator_message_processor.py # Fake backend
│ ├── assert_helpers.py # Assertion utilities
│ ├── any_compare_helpers.py # Flexible matchers (ANY, ANY_BUT_NONE)
│ ├── fake_message_factory.py # Message creation helpers
│ ├── noop_file_upload_manager.py # No-op file uploader
│ └── environment.py # Environment utilities
├── unit/ # Unit tests (no external dependencies)
│ ├── conftest.py # Unit test fixtures
│ ├── api_objects/ # Tests for API objects
│ │ ├── test_opik_client.py
│ │ ├── dataset/
│ │ ├── experiment/
│ │ ├── trace/
│ │ └── ...
│ ├── decorator/ # Decorator tests
│ │ ├── test_tracker_outputs.py # Comprehensive decorator tests
│ │ ├── test_dynamic_tracing.py
│ │ ├── test_span_context_manager.py
│ │ └── ...
│ ├── evaluation/ # Evaluation framework tests
│ │ ├── test_evaluate.py
│ │ ├── metrics/ # Metric tests
│ │ └── ...
│ ├── message_processing/ # Message processing tests
│ │ ├── test_message_streaming.py
│ │ ├── batching/
│ │ └── ...
│ └── ... # Other unit tests
├── library_integration/ # Integration tests with fake backend
│ ├── conftest.py # Shared fixtures
│ ├── openai/ # OpenAI integration tests
│ │ ├── requirements.txt
│ │ ├── constants.py
│ │ ├── test_openai_responses.py
│ │ └── ...
│ ├── anthropic/ # Anthropic integration tests
│ ├── langchain/ # LangChain integration tests
│ ├── bedrock/ # AWS Bedrock tests
│ ├── litellm/ # LiteLLM tests
│ └── ... # Other integrations
├── e2e/ # End-to-end tests (real backend)
│ ├── conftest.py # E2E fixtures
│ ├── verifiers.py # Backend verification helpers
│ ├── test_tracing.py # Core tracing tests
│ ├── test_dataset.py # Dataset tests
│ ├── test_prompt.py # Prompt tests
│ ├── evaluation/ # Evaluation E2E tests
│ └── ...
├── e2e_library_integration/ # E2E library integration (real backend)
│ ├── conftest.py # E2E lib integration fixtures
│ ├── litellm/ # LiteLLM E2E tests
│ ├── adk/ # ADK E2E tests
│ └── ...
└── e2e_smoke/ # Quick smoke tests
├── dry_run_import.py
└── smoke_tests_runner.sh
```
## Test Categories
### 1. Unit Tests (`tests/unit/`)
**Purpose**: Fast, isolated tests with no external dependencies.
**Characteristics**:
- Use fake backend (`fake_backend` fixture)
- No network calls
- Test internal logic and edge cases
- Run in milliseconds (almost always)
**Key Fixtures**:
```python
@pytest.fixture
def fake_backend(patch_streamer):
"""
Replaces Streamer with fake backend emulator.
Captures messages and builds trace/span trees.
Access via: fake_backend.trace_trees, fake_backend.span_trees
"""
```
**Example Structure**:
```python
def test_track__one_nested_function__happyflow(fake_backend):
@opik.track
def f_inner(x):
return "inner-output"
@opik.track
def f_outer(x):
f_inner("inner-input")
return "outer-output"
f_outer("outer-input")
opik.flush_tracker()
# Verify against expected tree structure
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="f_outer",
spans=[
SpanModel(name="f_outer", spans=[
SpanModel(name="f_inner", spans=[])
])
]
)
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
```
**What to Test**:
- Decorator behavior (input/output capture, nesting)
- Message creation and processing
- Batching logic
- Context management
- Error handling
- Metric calculations
- Data transformations
### 2. Library Integration Tests (`tests/library_integration/`)
**Purpose**: Test integrations with external libraries using fake backend.
**Characteristics**:
- Real integration library calls (OpenAI, LangChain, etc.)
- Fake Opik backend (no backend network calls)
- Verify tracing structure without backend dependency
- Requires API keys for external services
**Directory Structure**:
```
library_integration/
├── openai/
│ ├── requirements.txt # OpenAI-specific dependencies
│ ├── constants.py # Test constants (models, etc.)
│ ├── test_openai_responses.py
│ └── test_openai_chat_completions.py
├── anthropic/
├── langchain/
└── ...
```
**Example Structure**:
```python
def test_openai_client_responses_create__happyflow(fake_backend):
client = openai.OpenAI()
wrapped_client = track_openai(client, project_name="test")
# Real OpenAI API call
response = wrapped_client.responses.create(
model=MODEL_FOR_TESTS,
input=[{"role": "user", "content": "Hello"}]
)
opik.flush_tracker()
# Verify trace structure with fake backend
assert len(fake_backend.trace_trees) == 1
trace = fake_backend.trace_trees[0]
assert trace.name == "responses_create"
assert trace.spans[0].type == "llm"
assert trace.spans[0].provider == "openai"
```
**What to Test**:
- Integration decorator wrapping
- Input/output capture from library responses
- Usage tracking (tokens, costs)
- Provider-specific metadata
- Streaming responses
- Error handling
- Nested calls
**Requirements Files**:
Each integration has its own `requirements.txt`:
```txt
# openai/requirements.txt
openai>=1.0.0
# langchain/requirements.txt
langchain>=0.1.0
langchain-openai>=0.1.0
```
### 3. E2E Tests (`tests/e2e/`)
**Purpose**: Test core functionality against real Opik backend.
**Characteristics**:
- Real backend calls
- Slower (network + backend processing)
- Full system validation
- Requires configured Opik backend
**Key Fixtures**:
```python
@pytest.fixture()
def opik_client(configure_e2e_tests_env, shutdown_cached_client_after_test):
"""Real Opik client for E2E tests"""
opik_client_ = opik.Opik(_use_batching=True)
yield opik_client_
opik_client_.end()
@pytest.fixture
def dataset_name(opik_client):
"""Generate unique dataset name"""
name = f"e2e-tests-dataset-{random_chars()}"
yield name
```
**Example Structure**:
```python
def test_trace_creation_and_retrieval(opik_client, temporary_project_name):
# Create trace
trace_id = opik_client.trace(
name="test_trace",
input={"query": "test"},
project_name=temporary_project_name
)
opik_client.flush()
# Verify against real backend
verify_trace(
opik_client,
trace_id=trace_id,
name="test_trace",
input={"query": "test"},
project_name=temporary_project_name
)
```
**What to Test**:
- Trace/span creation and retrieval
- Dataset CRUD operations
- Experiment tracking
- Prompt management
- Feedback scores
- Attachments
- Search operations
- Thread management
**Verifiers (`verifiers.py`)**:
```python
def verify_trace(opik_client, trace_id, name, input, output, ...):
"""Wait for trace to appear in backend and verify fields"""
if not synchronization.until(
lambda: opik_client.get_trace_content(id=trace_id) is not None,
allow_errors=True
):
raise AssertionError(f"Failed to get trace {trace_id}")
trace = opik_client.get_trace_content(id=trace_id)
assert trace.name == name
assert trace.input == input
# ... more assertions
def verify_span(opik_client, span_id, ...):
"""Similar verification for spans"""
def verify_experiment_items(opik_client, experiment_id, expected_items):
"""Verify experiment items match expected"""
```
### 4. E2E Library Integration Tests (`tests/e2e_library_integration/`)
**Purpose**: Test library integrations against real backend.
**Characteristics**:
- Real library calls + Real backend calls
- Slowest test category
- Full integration validation
- Requires both service API keys and backend
**Example Structure**:
```python
def test_litellm_chat_model_e2e(opik_client_unique_project_name):
"""Test LiteLLM integration with real backend"""
from litellm import completion
from opik.integrations.litellm import track_litellm
track_litellm()
# Real LiteLLM call (which calls real LLM provider)
response = completion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Hello"}]
)
opik.flush_tracker()
# Verify in real backend
traces = opik_client_unique_project_name.search_traces()
assert len(traces) > 0
```
**When to Use**:
- Critical integration paths
- Features that require real backend state
- Complex multi-step workflows
- Release validation
### 5. Smoke Tests (`tests/e2e_smoke/`)
**Purpose**: Quick sanity checks that SDK can be imported and basic operations work.
**Example**:
```python
# dry_run_import.py
import opik
import opik.evaluation.metrics as metrics
# Verify basic imports work
client = opik.Opik()
```
## Testing Infrastructure
### Test Models (`testlib/models.py`)
Domain-specific models for test assertions:
```python
@dataclasses.dataclass
class SpanModel:
"""Represents expected span structure"""
id: str
name: Optional[str] = None
input: Any = None
output: Any = None
type: str = "general"
usage: Optional[Dict[str, Any]] = None
spans: List["SpanModel"] = dataclasses.field(default_factory=list)
# ... more fields
@dataclasses.dataclass
class TraceModel:
"""Represents expected trace structure"""
id: str
name: Optional[str]
input: Any = None
output: Any = None
spans: List[SpanModel] = dataclasses.field(default_factory=list)
# ... more fields
@dataclasses.dataclass
class FeedbackScoreModel:
"""Represents expected feedback score"""
id: str
name: str
value: float
reason: Optional[str] = None
```
### Fake Backend (`testlib/backend_emulator_message_processor.py`)
**Purpose**: Emulate backend behavior for unit and library integration tests.
**Key Features**:
- Processes messages without network calls
- Builds trace and span trees from messages in memory
- Supports duplicate merging (simulates backend behavior)
- Tracks feedback scores and attachments
```python
class BackendEmulatorMessageProcessor(BaseMessageProcessor):
def __init__(self, merge_duplicates: bool = True):
self.processed_messages: List[messages.BaseMessage] = []
self._trace_trees: List[TraceModel] = []
self._span_trees: List[SpanModel] = []
# ... internal state
@property
def trace_trees(self) -> List[TraceModel]:
"""Build and return trace trees from processed messages"""
@property
def span_trees(self) -> List[SpanModel]:
"""Build and return span trees from processed messages"""
def process(self, message: messages.BaseMessage) -> None:
"""Process message and update internal state"""
```
**Usage**:
```python
def test_example(fake_backend):
# Execute code that creates traces/spans
@opik.track
def my_function():
return "result"
my_function()
opik.flush_tracker()
# Access built trees
assert len(fake_backend.trace_trees) == 1
assert fake_backend.trace_trees[0].name == "my_function"
```
### Flexible Matchers (`testlib/any_compare_helpers.py`)
Special matchers for flexible assertions:
```python
ANY = SpecialValue("ANY") # Matches anything
ANY_BUT_NONE = SpecialValue("ANY_BUT_NONE") # Matches anything except None
ANY_STRING = StringMatcher() # String-specific matcher
ANY_DICT = DictMatcher() # Dict-specific matcher
# Usage
assert_equal(
expected=TraceModel(
id=ANY_BUT_NONE, # Don't care about ID, but must exist
name="test",
start_time=ANY_BUT_NONE, # Don't care about time but must exist
input={"key": "value"} # Exact match
),
actual=fake_backend.trace_trees[0]
)
# String matchers
ANY_STRING.starting_with("gpt-")
ANY_STRING.ending_with(".txt")
ANY_STRING.containing("test")
```
### Assertion Helpers (`testlib/assert_helpers.py`)
```python
def assert_equal(expected, actual):
"""
Deep equality check with support for:
- SpecialValue matchers (ANY, ANY_BUT_NONE)
- Nested dataclasses
- Lists and dicts
- Provides detailed diff on mismatch
"""
def assert_dict_has_keys(dict_obj, required_keys):
"""Verify dict contains all required keys"""
```
### Fixtures
#### Root Fixtures (`tests/conftest.py`)
```python
@pytest.fixture(autouse=True)
def clear_context_storage():
"""Automatically clear context after each test"""
yield
context_storage.clear_all()
@pytest.fixture(autouse=True)
def shutdown_cached_client_after_test():
"""Clean up cached Opik client after each test"""
yield
if opik_client.get_client_cached.cache_info().currsize > 0:
opik_client.get_client_cached().end()
opik_client.get_client_cached.cache_clear()
@pytest.fixture
def fake_backend(patch_streamer):
"""Fake backend for unit/library integration tests"""
streamer, fake_message_processor = patch_streamer
# ... setup
yield fake_message_processor
# ... cleanup
@pytest.fixture
def patch_streamer():
"""Create streamer with fake backend"""
fake_processor = BackendEmulatorMessageProcessor()
fake_upload_manager = NoopFileUploadManager()
streamer = streamer_constructors.construct_streamer(
message_processor=fake_processor,
n_consumers=1,
use_batching=True,
file_uploader=fake_upload_manager,
max_queue_size=None
)
yield streamer, fake_processor
streamer.close(timeout=5)
```
#### E2E Fixtures (`tests/e2e/conftest.py`)
```python
@pytest.fixture()
def opik_client(configure_e2e_tests_env):
"""Real Opik client with batching enabled"""
client = opik.Opik(_use_batching=True)
yield client
client.end()
@pytest.fixture
def dataset_name(opik_client):
"""Generate unique dataset name for test"""
name = f"e2e-tests-dataset-{random_chars()}"
yield name
@pytest.fixture
def temporary_project_name(opik_client):
"""Create and cleanup temporary project"""
name = f"e2e-tests-temporary-project-{random_chars()}"
yield name
# Cleanup
project_id = opik_client.rest_client.projects.retrieve_project(name=name).id
opik_client.rest_client.projects.delete_project_by_id(project_id)
```
#### Library Integration Fixtures
```python
# tests/library_integration/conftest.py
@pytest.fixture(autouse=True)
def reset_tracing_to_config_default():
"""Reset tracing config between tests"""
opik.reset_tracing_to_config_default()
yield
opik.reset_tracing_to_config_default()
# tests/library_integration/openai/conftest.py
@pytest.fixture
def ensure_openai_configured():
"""Verify OpenAI API key is configured"""
if not os.getenv("OPENAI_API_KEY"):
pytest.skip("OPENAI_API_KEY not configured")
```
## Testing Patterns
### Pattern 1: Testing Decorator Behavior
**Location**: `tests/unit/decorator/test_tracker_outputs.py`
```python
def test_track__one_nested_function__happyflow(fake_backend):
"""
Test naming convention:
test_WHAT__CASE_DESCRIPTION__EXPECTED_RESULT
"""
@opik.track
def f_inner(x):
return "inner-output"
@opik.track
def f_outer(x):
f_inner("inner-input")
return "outer-output"
f_outer("outer-input")
opik.flush_tracker() # Wait for async processing
# Build expected tree structure
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="f_outer",
input={"x": "outer-input"},
output={"output": "outer-output"},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
name="f_outer",
input={"x": "outer-input"},
output={"output": "outer-output"},
spans=[
SpanModel(
name="f_inner",
input={"x": "inner-input"},
output={"output": "inner-output"},
spans=[]
)
]
)
]
)
assert len(fake_backend.trace_trees) == 1
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
```
### Pattern 2: Testing Integration Tracking
**Location**: `tests/library_integration/openai/test_openai_responses.py`
```python
@pytest.mark.parametrize(
"project_name, expected_project_name",
[
(None, OPIK_PROJECT_DEFAULT_NAME),
("custom-project", "custom-project"),
],
)
def test_openai_client_responses_create__happyflow(
fake_backend, project_name, expected_project_name
):
# Setup integration
client = openai.OpenAI()
wrapped_client = track_openai(client, project_name=project_name)
# Real API call
response = wrapped_client.responses.create(
model=MODEL_FOR_TESTS,
input=[{"role": "user", "content": "Tell a fact"}],
max_output_tokens=50
)
opik.flush_tracker()
# Build expected structure
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="responses_create",
input={"input": ANY_BUT_NONE},
output={"output": ANY_BUT_NONE, "reasoning": ANY},
tags=["openai"],
metadata=ANY_DICT,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
project_name=expected_project_name,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="llm",
name="responses_create",
provider="openai",
model=ANY_STRING.starting_with(MODEL_FOR_TESTS),
usage=ANY_BUT_NONE,
metadata=ANY_DICT,
tags=["openai"],
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[]
)
]
)
assert len(fake_backend.trace_trees) == 1
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
# Optional: Verify specific metadata keys if needed
assert_dict_has_keys(
fake_backend.trace_trees[0].spans[0].metadata,
["created_from", "model"]
)
```
### Pattern 3: Testing E2E with Backend Verification
**Location**: `tests/e2e/test_tracing.py`
```python
def test_trace_creation_with_spans(opik_client, temporary_project_name):
# Create trace
trace_id = opik_client.trace(
name="parent_trace",
input={"query": "test"},
project_name=temporary_project_name
)
# Create spans
span_id_1 = opik_client.span(
name="span_1",
trace_id=trace_id,
input={"step": 1}
)
span_id_2 = opik_client.span(
name="span_2",
trace_id=trace_id,
parent_span_id=span_id_1,
input={"step": 2}
)
opik_client.flush()
# Verify in backend
verify_trace(
opik_client,
trace_id=trace_id,
name="parent_trace",
input={"query": "test"},
project_name=temporary_project_name
)
verify_span(
opik_client,
span_id=span_id_1,
name="span_1",
trace_id=trace_id,
parent_span_id=None
)
verify_span(
opik_client,
span_id=span_id_2,
name="span_2",
trace_id=trace_id,
parent_span_id=span_id_1
)
```
### Pattern 4: Testing Error Handling
```python
def test_track__function_raises_exception__error_info_captured(fake_backend):
@opik.track
def failing_function():
raise ValueError("Test error")
with pytest.raises(ValueError, match="Test error"):
failing_function()
opik.flush_tracker()
# Build expected structure with error_info
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="failing_function",
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
name="failing_function",
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
error_info={
"exception_type": "ValueError",
"message": ANY_STRING.containing("Test error"),
"traceback": ANY_BUT_NONE
},
spans=[]
)
]
)
assert len(fake_backend.trace_trees) == 1
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
```
### Pattern 5: Testing Streaming Responses
```python
def test_openai_streaming_response(fake_backend):
client = openai.OpenAI()
wrapped_client = track_openai(client)
# Stream response
stream = wrapped_client.chat.completions.create(
model=MODEL_FOR_TESTS,
messages=[{"role": "user", "content": "Count to 5"}],
stream=True
)
# Consume stream
for chunk in stream:
pass # Consume all chunks
opik.flush_tracker()
# Verify accumulated data using models
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="chat_completions_create",
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
name="chat_completions_create",
type="llm",
provider="openai",
model=ANY_STRING.starting_with(MODEL_FOR_TESTS),
usage=ANY_BUT_NONE, # Usage accumulated from chunks
output=ANY_BUT_NONE, # Output accumulated from chunks
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[]
)
]
)
assert len(fake_backend.trace_trees) == 1
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
```
### Pattern 6: Testing Metrics
```python
def test_hallucination_metric__happyflow():
metric = Hallucination()
result = metric.score(
input="What is the capital of France?",
output="Paris is the capital of France.",
context=["Paris is the capital and largest city of France."]
)
assert isinstance(result, ScoreResult)
assert 0 <= result.value <= 1
assert result.name == "hallucination_metric"
assert result.reason is not None
```
## Writing Tests
### Test Naming Convention
Follow the pattern: `test_WHAT__CASE_DESCRIPTION__EXPECTED_RESULT`
```python
# ✅ Good
def test_track__one_nested_function__happyflow(fake_backend):
def test_track__function_raises_exception__error_info_captured(fake_backend):
def test_evaluate__with_custom_metric__scores_computed_correctly(fake_backend):
# ❌ Bad
def test_tracking():
def test_error():
def test_evaluate():
```
### Using Fake Backend
```python
def test_my_feature(fake_backend):
# 1. Execute code that creates traces/spans
@opik.track
def my_function(x):
return x * 2
result = my_function(5)
opik.flush_tracker() # Always flush!
# 2. Build expected structure
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="my_function",
input={"x": 5},
output={"output": 10},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
name="my_function",
input={"x": 5},
output={"output": 10},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[]
)
]
)
# 3. Assert
assert len(fake_backend.trace_trees) == 1
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
```
### Testing with Real Backend
```python
def test_my_e2e_feature(opik_client, temporary_project_name):
# 1. Create resources
trace_id = opik_client.trace(
name="test_trace",
project_name=temporary_project_name
)
opik_client.flush()
# 2. Verify using verifiers
verify_trace(
opik_client,
trace_id=trace_id,
name="test_trace",
project_name=temporary_project_name
)
```
### Parametrized Tests
```python
@pytest.mark.parametrize(
"input_value, expected_output",
[
(5, 10),
(10, 20),
(0, 0),
],
)
def test_double_function__various_inputs__correct_outputs(
fake_backend, input_value, expected_output
):
@opik.track
def double(x):
return x * 2
result = double(input_value)
opik.flush_tracker()
assert len(fake_backend.trace_trees) == 1
assert fake_backend.trace_trees[0].spans[0].output == {"output": expected_output}
```
### Integration Test Requirements
Each integration should have:
1. `requirements.txt` with integration dependencies
2. `conftest.py` with integration-specific fixtures
3. `constants.py` for test constants (models, etc.)
4. Tests for main integration features
```python
# library_integration/myintegration/requirements.txt
myintegration>=1.0.0
# library_integration/myintegration/conftest.py
import pytest
import os
@pytest.fixture
def ensure_myintegration_configured():
if not os.getenv("MYINTEGRATION_API_KEY"):
pytest.skip("MYINTEGRATION_API_KEY not configured")
# library_integration/myintegration/test_myintegration.py
def test_myintegration_basic(fake_backend, ensure_myintegration_configured):
# Test implementation
```
## Running Tests
### Run All Tests
```bash
pytest tests/
```
### Run Specific Category
```bash
# Unit tests only (fast)
pytest tests/unit/
# Library integration tests
pytest tests/library_integration/
# E2E tests
pytest tests/e2e/
# Specific integration
pytest tests/library_integration/openai/
```
### Environment Variables
Some library integration and E2E tests require certain environment variables to be configured:
```bash
# Backend configuration
export OPIK_URL_OVERRIDE="http://localhost:5000"
export OPIK_API_KEY="your_api_key"
# LLM provider keys (for library integration tests)
export OPENAI_API_KEY="..."
export ANTHROPIC_API_KEY="..."
export GOOGLE_API_KEY="..."
```
## Best Practices
1. **Always Use Fake Backend for Unit and Library Integration Tests**: Avoid network calls
2. **Test Public API Only**: Don't test private methods
3. **Use Flexible Matchers**: Use `ANY`, `ANY_BUT_NONE` for non-critical fields
4. **Build Expected Structures**: Make tests readable with clear expected output
5. **Clean Up Resources**: Use fixtures for cleanup (especially E2E tests)
6. **Parametrize Similar Tests**: Reduce duplication with `@pytest.mark.parametrize`
7. **Document Test Purpose**: Use clear names and docstrings
8. **Test Edge Cases**: Include error cases, empty inputs, etc.
9. **Keep Tests Fast**: Unit tests should run in milliseconds
10. **Use Verifiers for E2E**: Leverage existing verification helpers
For more information, see:
- [API and Data Flow](API_AND_DATA_FLOW.md) - Core architecture and data flow
- [Integrations](INTEGRATIONS.md) - Integration patterns and testing
- [Evaluation](EVALUATION.md) - Evaluation framework architecture
- [Test Organization Rules](../../../.agents/skills/python-sdk/testing.md)
- [Test Implementation Rules](../../../.agents/skills/python-sdk/good-code.md)