Skip to main content

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

  1. Test Script Anatomy
  2. Working Directory Management
  3. Path Resolution and Imports
  4. Test Execution Lifecycle
  5. Real-World Examples
  6. Common Patterns Explained
  7. 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

  1. Consistent Behavior: Tests behave the same from any location
  2. Relative Paths Work: Any relative file paths in tests work correctly
  3. Predictable Environment: Working directory is always known
  4. 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:

  1. Consistency: Same API interface across all tests
  2. Simplicity: One client library to maintain
  3. Cross-Platform: Python works everywhere
  4. 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

  1. Always Change Directory: Use os.chdir(script_dir) at the start
  2. Use Unique Names: Timestamps prevent conflicts
  3. Track Results: Count pass/fail for each scenario
  4. Clean Up: Always delete test data
  5. Handle Errors: Test both success and failure paths
  6. Isolate Tests: Each test should be independent
  7. Provide Context: Print what's being tested
  8. Show Values: Display actual vs expected
  9. Use Real Data: Tests should simulate real usage
  10. Document Assumptions: Comment on expected behavior

Next Steps

  1. Run a Test: Execute a single test to see it work
  2. Modify a Test: Change a test to match your needs
  3. Create a Test: Write a new test for your scenario
  4. Read the Code: Study test implementations
  5. Check Documentation: Review API documentation

For more information: