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
1035 lines
29 KiB
Markdown
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)
|