How Tests Work: Complete Guide
This document provides an in-depth explanation of how the hlquery test suite works, from script execution to result interpretation.
Table of Contents
- Test Script Anatomy
- Working Directory Management
- Path Resolution and Imports
- Test Execution Lifecycle
- Real-World Examples
- Common Patterns Explained
- Debugging and Troubleshooting
Test Script Anatomy
Complete Test Script Structure
Every test script follows this exact structure:
#!/usr/bin/env python3
"""
Test Description - What this test does
"""
import sys
import os
import time # If needed for timestamps
# ----------------------------------------------------------------====
# STEP 1: Change to Script Directory
# ----------------------------------------------------------------====
# This is CRITICAL - ensures tests work from any location
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
# Now the working directory is always the test directory
# ----------------------------------------------------------------====
# STEP 2: Setup Python Path for Imports
# ----------------------------------------------------------------====
# Calculate path to Python API directory
# From: api/perl/tests/test_search.py
# To: api/python/
python_api_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(script_dir))),
'python'
)
sys.path.insert(0, python_api_path)
# ----------------------------------------------------------------====
# STEP 3: Import Client Library
# ----------------------------------------------------------------====
from lib import Client
# ----------------------------------------------------------------====
# STEP 4: Configuration
# ----------------------------------------------------------------====
BASE_URL = 'http://localhost:9200'
# ----------------------------------------------------------------====
# STEP 5: Test Function
# ----------------------------------------------------------------====
def test_feature():
"""Main test function"""
print("=" * 70)
print("TEST: Feature Name (API Name)")
print("-" * 70)
# Initialize client
client = Client(BASE_URL)
tests_passed = 0
tests_failed = 0
# Test scenarios here...
# Summary
print("\n" + "=" * 70)
print(f"Test Summary: {tests_passed} passed, {tests_failed} failed")
print("=" * 70 + "\n")
# ----------------------------------------------------------------====
# STEP 6: Entry Point
# ----------------------------------------------------------------====
if __name__ == '__main__':
test_feature()
Working Directory Management
Why Change Working Directory?
Problem: If you run a test from different directories, relative paths break:
# From project root - works
$ python api/perl/tests/test_search.py
# From /tmp - breaks!
$ cd /tmp
$ python /home/cferry/work/hlquery/api/perl/tests/test_search.py
# Error: Can't find relative files, wrong paths, etc.
Solution: Every test script changes to its own directory first:
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
How It Works
# Example: Running from /tmp
# File: /home/cferry/work/hlquery/api/perl/tests/test_search.py
# Step 1: Get absolute path of script
__file__ = '/home/cferry/work/hlquery/api/perl/tests/test_search.py'
os.path.abspath(__file__) # Same, already absolute
# Step 2: Get directory containing script
os.path.dirname(__file__) # '/home/cferry/work/hlquery/api/perl/tests'
script_dir = '/home/cferry/work/hlquery/api/perl/tests'
# Step 3: Change to that directory
os.chdir(script_dir)
# Now: os.getcwd() == '/home/cferry/work/hlquery/api/perl/tests'
# Result: Working directory is ALWAYS the test directory
# No matter where you run the script from!
Benefits
- Consistent Behavior: Tests behave the same from any location
- Relative Paths Work: Any relative file paths in tests work correctly
- Predictable Environment: Working directory is always known
- No Path Confusion: No need to calculate relative paths from current directory
Example: Running from Different Locations
# Scenario 1: From project root
$ cd /home/cferry/work/hlquery
$ python api/perl/tests/test_search.py
# Working directory becomes: /home/cferry/work/hlquery/api/perl/tests
# Scenario 2: From /tmp
$ cd /tmp
$ python /home/cferry/work/hlquery/api/perl/tests/test_search.py
# Working directory becomes: /home/cferry/work/hlquery/api/perl/tests
# (Same as Scenario 1!)
# Scenario 3: From test directory itself
$ cd /home/cferry/work/hlquery/api/perl/tests
$ python test_search.py
# Working directory becomes: /home/cferry/work/hlquery/api/perl/tests
# (Same as both scenarios above!)
Path Resolution and Imports
Understanding the Path Calculation
The path to the Python API is calculated relative to the script's location:
# From: api/perl/tests/test_search.py
# To: api/python/
# Step-by-step:
script_dir = '/home/cferry/work/hlquery/api/perl/tests'
# Go up one level: tests -> perl
parent1 = os.path.dirname(script_dir) # '/home/cferry/work/hlquery/api/perl'
# Go up one level: perl -> api
parent2 = os.path.dirname(parent1) # '/path/to/hlquery/api'
# Go up one level: api -> etc (but we want api/python)
# Actually, we want: api/python, so:
python_api_path = os.path.join(parent2, 'python')
# Result: '/home/cferry/work/hlquery/api/python'
Visual Path Tree
api/
├── python/ ← Target: import from here
│ └── lib/
│ └── __init__.py
├── perl/
│ └── tests/
│ └── test_search.py ← Starting point
├── php/
├── node/
├── cpp/
└── rust/
Path calculation:
- Start:
api/perl/tests/test_search.py - Up 1:
api/perl/tests/→api/perl/ - Up 2:
api/perl/→api/ - Join:
api/+python/→api/python/
Why This Works for All APIs
The same Python client library is used for all API tests:
# In perl/tests/test_search.py
from lib import Client # Imports from api/python/lib/
# In php/tests/test_search.py
from lib import Client # Imports from api/python/lib/
# In node/tests/test_search.py
from lib import Client # Imports from api/python/lib/
All tests use the Python client because:
- Consistency: Same API interface across all tests
- Simplicity: One client library to maintain
- Cross-Platform: Python works everywhere
- Feature Parity: All APIs get the same test coverage
Test Execution Lifecycle
Phase-by-Phase Breakdown
Phase 1: Script Initialization (0-50ms)
# What happens:
1. Python interpreter loads script
2. Imports sys, os, time modules
3. Calculates script_dir
4. Changes working directory
5. Calculates python_api_path
6. Adds path to sys.path
7. Imports Client class
Timeline:
0ms: Script starts
5ms: Imports loaded
10ms: script_dir calculated
15ms: os.chdir() executed
20ms: python_api_path calculated
25ms: sys.path updated
30ms: Client imported
50ms: Ready to run test
Phase 2: Test Setup (50-200ms)
def test_search():
# Initialize
client = Client(BASE_URL) # Creates HTTP client
tests_passed = 0
tests_failed = 0
# Print header
print("=" * 70)
print("TEST: Search Functionality")
print("-" * 70)
What happens:
- Client object created (connects to server)
- Counters initialized
- Header printed
Phase 3: Data Preparation (200-500ms)
# Create test collection
collection_name = 'test_search_' + str(int(time.time()))
schema = {
'fields': [
{'name': 'title', 'type': 'string'},
{'name': 'content', 'type': 'string'}
]
}
create_result = client.collections_api().create(collection_name, schema)
# Add test documents
test_docs = [
{'id': 'doc1', 'title': 'Test Document', 'content': 'Test content'},
{'id': 'doc2', 'title': 'Another Test', 'content': 'More content'}
]
for doc in test_docs:
client.documents_api().add(collection_name, doc)
What happens:
- Collection created via HTTP POST
- Documents added via HTTP POST
- Server processes and indexes data
Timeline:
200ms: Collection creation request sent
250ms: Collection created, response received
300ms: First document added
350ms: Second document added
400ms: All documents indexed
500ms: Ready for testing
Phase 4: Test Execution (500ms-2s)
# Test 1: Basic search
print("Test 1: Basic search")
search_params = {
'q': 'test',
'query_by': 'title,content',
'limit': 10
}
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
print(f" ✓ Found {len(hits)} results")
tests_passed += 1
else:
print(f" ✗ Search failed: {results.get_status_code()}")
tests_failed += 1
What happens:
- HTTP GET request to search endpoint
- Server processes search query
- Results returned as JSON
- Response validated
- Pass/fail tracked
Timeline:
500ms: Search request sent
550ms: Server processes query
600ms: Results returned
650ms: Response validated
700ms: Test 1 complete
Phase 5: Cleanup (2s-2.5s)
# Cleanup
delete_result = client.collections_api().delete(collection_name)
if delete_result.is_success():
print(f"\n✓ Cleaned up collection: {collection_name}")
What happens:
- HTTP DELETE request
- Collection and all documents removed
- Server cleans up indexes
Phase 6: Summary (2.5s-2.6s)
print("\n" + "=" * 70)
print(f"Test Summary: {tests_passed} passed, {tests_failed} failed")
print("=" * 70 + "\n")
What happens:
- Final statistics printed
- Test completes
Real-World Examples
Example 1: Complete Test Walkthrough
Let's trace through test_search.py step by step:
#!/usr/bin/env python3
"""
Search Test - Tests search functionality
"""
import sys
import os
import time
# ----------------------------------------------------------------====
# INITIALIZATION
# ----------------------------------------------------------------====
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
# Current directory: /home/cferry/work/hlquery/api/perl/tests
python_api_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(script_dir))),
'python'
)
# python_api_path: /home/cferry/work/hlquery/api/python
sys.path.insert(0, python_api_path)
from lib import Client
BASE_URL = 'http://localhost:9200'
# ----------------------------------------------------------------====
# TEST FUNCTION
# ----------------------------------------------------------------====
def test_search():
print("=" * 70)
print("TEST: Search Functionality (Perl API)")
print("-" * 70)
# Create client
client = Client(BASE_URL)
# Client object created, ready to make HTTP requests
tests_passed = 0
tests_failed = 0
# ----------------------------------------------------------------====
# SETUP: Create Test Data
# ----------------------------------------------------------------====
collection_name = 'test_search_perl_' + str(int(time.time()))
# Example: 'test_search_perl_1704123456'
schema = {
'fields': [
{'name': 'title', 'type': 'string'},
{'name': 'content', 'type': 'string'},
{'name': 'category', 'type': 'string'},
{'name': 'price', 'type': 'float'}
]
}
print(f"Creating test collection: {collection_name}")
create_result = client.collections_api().create(collection_name, schema)
# HTTP POST /collections
# Body: {"fields": [...]}
# Response: 201 Created
if not create_result.is_success():
print(f"✗ Failed to create collection: {create_result.get_status_code()}")
return
print(f"✓ Collection created")
# Add test documents
test_docs = [
{
'id': 'doc1',
'title': 'Test Document One',
'content': 'This is test content for searching',
'category': 'test',
'price': 10.0
},
{
'id': 'doc2',
'title': 'Test Document Two',
'content': 'Another test document with content',
'category': 'test',
'price': 20.0
},
{
'id': 'doc3',
'title': 'Sample Document',
'content': 'Sample content here',
'category': 'sample',
'price': 15.0
}
]
print(f"Adding {len(test_docs)} test documents...")
for doc in test_docs:
add_result = client.documents_api().add(collection_name, doc)
# HTTP POST /collections/{name}/documents
# Body: {"id": "doc1", "title": "...", ...}
# Response: 201 Created
if not add_result.is_success():
print(f" ✗ Failed to add document {doc['id']}")
print(f"✓ Documents added")
print(f"\nUsing collection: {collection_name}\n")
# ----------------------------------------------------------------====
# TEST 1: Basic Search
# ----------------------------------------------------------------====
print("Test 1: Basic search")
search_params = {
'q': 'test', # Search query
'query_by': 'title,content', # Search in these fields
'limit': 10 # Return up to 10 results
}
# HTTP GET /collections/test_search_perl_1704123456/documents/search?q=test&query_by=title,content&limit=10
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
print(f" ✓ Found {len(hits)} results")
# Expected: 2 results (doc1 and doc2 contain "test")
tests_passed += 1
else:
print(f" ✗ Search failed: {results.get_status_code()}")
tests_failed += 1
# ----------------------------------------------------------------====
# TEST 2: Filtered Search
# ----------------------------------------------------------------====
print("\nTest 2: Search with filters")
search_params = {
'q': '*', # Search all
'query_by': 'title', # Search in title
'filter_by': 'category:test', # Filter by category
'limit': 5
}
# HTTP GET /collections/.../search?q=*&query_by=title&filter_by=category:test&limit=5
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
print(f" ✓ Filtered search found {len(hits)} results")
# Expected: 2 results (only doc1 and doc2 have category='test')
tests_passed += 1
else:
print(f" ✗ Filtered search failed: {results.get_status_code()}")
tests_failed += 1
# ----------------------------------------------------------------====
# TEST 3: Sorted Search
# ----------------------------------------------------------------====
print("\nTest 3: Search with sorting")
search_params = {
'q': '*',
'query_by': 'title',
'sort_by': 'price:asc', # Sort by price ascending
'limit': 10
}
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
if len(hits) >= 3:
prices = [hit.get('document', {}).get('price', 0) for hit in hits]
if prices == sorted(prices):
print(f" ✓ Results sorted correctly by price")
tests_passed += 1
else:
print(f" ✗ Results not sorted correctly")
tests_failed += 1
else:
print(f" ✗ Expected 3 results, got {len(hits)}")
tests_failed += 1
else:
print(f" ✗ Sorted search failed: {results.get_status_code()}")
tests_failed += 1
# ----------------------------------------------------------------====
# TEST 4: Paginated Search
# ----------------------------------------------------------------====
print("\nTest 4: Search with pagination")
search_params = {
'q': '*',
'query_by': 'title,content',
'offset': 0, # Start from first result
'limit': 2 # Return only 2 results
}
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
if len(hits) == 2:
print(f" ✓ Pagination returned {len(hits)} results as expected")
tests_passed += 1
else:
print(f" ✗ Expected 2 results, got {len(hits)}")
tests_failed += 1
else:
print(f" ✗ Paginated search failed: {results.get_status_code()}")
tests_failed += 1
# ----------------------------------------------------------------====
# TEST 5: Wildcard Search
# ----------------------------------------------------------------====
print("\nTest 5: Wildcard search")
search_params = {
'q': 'test*', # Wildcard: matches "test", "testing", etc.
'query_by': 'title',
'limit': 10
}
results = client.search(collection_name, search_params)
if results.is_success():
body = results.get_body()
hits = body.get('hits', [])
print(f" ✓ Wildcard search found {len(hits)} results")
# Expected: 2 results (doc1 and doc2 titles start with "Test")
tests_passed += 1
else:
print(f" ✗ Wildcard search failed: {results.get_status_code()}")
tests_failed += 1
# ----------------------------------------------------------------====
# CLEANUP
# ----------------------------------------------------------------====
print(f"\nCleaning up test collection: {collection_name}")
delete_result = client.collections_api().delete(collection_name)
# HTTP DELETE /collections/test_search_perl_1704123456
# Response: 200 OK
if delete_result.is_success():
print(f"✓ Cleaned up collection: {collection_name}")
else:
print(f"✗ Failed to delete collection: {delete_result.get_status_code()}")
# ----------------------------------------------------------------====
# SUMMARY
# ----------------------------------------------------------------====
print("\n" + "=" * 70)
print(f"Search Test Summary: {tests_passed} passed, {tests_failed} failed")
print("=" * 70 + "\n")
if __name__ == '__main__':
test_search()
Example 2: HTTP Request Flow
Here's what actually happens when a test makes an API call:
# Test code:
results = client.search(collection_name, search_params)
# What happens internally:
# 1. Client builds URL
url = f"{BASE_URL}/collections/{collection_name}/documents/search"
# Result: "http://localhost:9200/collections/test_search_perl_1704123456/documents/search"
# 2. Client builds query parameters
params = {
'q': 'test',
'query_by': 'title,content',
'limit': 10
}
# URL becomes: "http://localhost:9200/collections/.../search?q=test&query_by=title,content&limit=10"
# 3. Client makes HTTP GET request
# Request:
# GET /collections/test_search_perl_1704123456/documents/search?q=test&query_by=title,content&limit=10 HTTP/1.1
# Host: localhost:9200
# User-Agent: Python-requests/2.31.0
# 4. Server processes request
# - Parses query parameters
# - Searches collection index
# - Finds matching documents
# - Ranks results
# - Returns top 10
# 5. Server sends response
# Response:
# HTTP/1.1 200 OK
# Content-Type: application/json
#
# {
# "hits": [
# {
# "document": {"id": "doc1", "title": "Test Document One", ...},
# "highlights": [...],
# "rank": 1
# },
# {
# "document": {"id": "doc2", "title": "Test Document Two", ...},
# "highlights": [...],
# "rank": 2
# }
# ],
# "found": 2,
# "page": 1
# }
# 6. Client parses response
response = Response(status_code=200, body={...})
# 7. Test checks result
if response.is_success(): # 200 is success
tests_passed += 1
Common Patterns Explained
Pattern 1: Create-Use-Cleanup
When to use: Testing operations that modify data
# CREATE
collection_name = 'test_' + str(int(time.time()))
client.collections_api().create(collection_name, schema)
# USE
# ... perform operations ...
# CLEANUP
client.collections_api().delete(collection_name)
Why this pattern:
- Isolation: Each test has its own data
- No Conflicts: Timestamp ensures unique names
- Clean State: No leftover data after test
Pattern 2: Use Existing or Create
When to use: Tests that can work with existing data
# Try existing first
collections = client.list_collections(0, 1)
if collections.is_success():
body = collections.get_body()
if body.get('collections'):
collection_name = body['collections'][0]['name']
use_existing = True
# Create if needed
if not use_existing:
collection_name = 'test_' + str(int(time.time()))
client.collections_api().create(collection_name, schema)
created_collection = True
# ... use collection ...
# Cleanup only if we created it
if created_collection:
client.collections_api().delete(collection_name)
Why this pattern:
- Faster: Reuses existing data
- Flexible: Works with or without existing data
- Safe: Only cleans up what we created
Pattern 3: Pass/Fail Tracking
When to use: All tests with multiple scenarios
tests_passed = 0
tests_failed = 0
# Test scenario 1
result = client.operation1()
if result.is_success():
tests_passed += 1
else:
tests_failed += 1
# Test scenario 2
result = client.operation2()
if result.is_success():
tests_passed += 1
else:
tests_failed += 1
# Summary
print(f"Summary: {tests_passed} passed, {tests_failed} failed")
Why this pattern:
- Clear Results: Know exactly what passed/failed
- Detailed Reporting: See per-scenario results
- CI/CD Friendly: Easy to parse results
Pattern 4: Error Validation
When to use: Testing error handling
# Test should fail with 404
result = client.get_collection('__nonexistent__')
if not result.is_success() and result.get_status_code() == 404:
print(" ✓ Correctly returned 404")
tests_passed += 1
else:
print(f" ✗ Unexpected: {result.get_status_code()}")
tests_failed += 1
Why this pattern:
- Validates Errors: Ensures errors are handled correctly
- Edge Cases: Tests boundary conditions
- Robustness: Verifies system handles invalid input
Debugging and Troubleshooting
Problem: Test Fails with Import Error
Symptom:
ImportError: No module named 'lib'
Cause: Python path not set correctly
Solution: Check path calculation:
# Debug: Print paths
script_dir = os.path.dirname(os.path.abspath(__file__))
print(f"Script dir: {script_dir}")
python_api_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(script_dir))),
'python'
)
print(f"Python API path: {python_api_path}")
print(f"Path exists: {os.path.exists(python_api_path)}")
print(f"lib exists: {os.path.exists(os.path.join(python_api_path, 'lib'))}")
Problem: Test Fails with Connection Error
Symptom:
ConnectionError: Connection refused
Cause: Server not running
Solution: Start server first:
# Check if server is running
curl http://localhost:9200/health
# If not running, start it
./run/hlquery start
Problem: Test Creates Data But Doesn't Clean Up
Symptom: Collections left after test runs
Cause: Test crashed before cleanup
Solution: Add try/finally:
def test_feature():
collection_name = 'test_' + str(int(time.time()))
created_collection = False
try:
# Create collection
client.collections_api().create(collection_name, schema)
created_collection = True
# ... run tests ...
finally:
# Always cleanup
if created_collection:
client.collections_api().delete(collection_name)
Problem: Tests Fail When Run from Different Directory
Symptom: Tests work from test directory but fail from elsewhere
Cause: Missing os.chdir() call
Solution: Ensure all tests have:
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
Problem: Test Times Out
Symptom: Test hangs or times out
Cause: Server not responding or infinite loop
Solution: Add timeout and debug output:
import signal
def timeout_handler(signum, frame):
raise TimeoutError("Test timed out")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(60) # 60 second timeout
try:
# Run test
test_feature()
finally:
signal.alarm(0) # Cancel timeout
Best Practices Summary
- Always Change Directory: Use
os.chdir(script_dir)at the start - Use Unique Names: Timestamps prevent conflicts
- Track Results: Count pass/fail for each scenario
- Clean Up: Always delete test data
- Handle Errors: Test both success and failure paths
- Isolate Tests: Each test should be independent
- Provide Context: Print what's being tested
- Show Values: Display actual vs expected
- Use Real Data: Tests should simulate real usage
- Document Assumptions: Comment on expected behavior
Next Steps
- Run a Test: Execute a single test to see it work
- Modify a Test: Change a test to match your needs
- Create a Test: Write a new test for your scenario
- Read the Code: Study test implementations
- Check Documentation: Review API documentation
For more information:
- Main Testing Guide - Overview of all tests
- Testing Examples - More examples and patterns
- Detailed Testing Guide - Deep dives into specific tests