This directory contains integration tests for Lightspeed Core Stack. Integration tests verify that multiple components work together correctly, using real implementations where possible and mocking only external dependencies.
- Getting Started
- Common Fixtures
- Helper Functions
- Test Constants
- Writing Integration Tests
- Running Tests
- Data-Driven (Parameterized) Tests
- Best Practices
Integration tests are located in subdirectories:
endpoints/- Tests for REST API endpoints- Other modules test specific components in isolation
All integration tests share common fixtures and helpers defined in conftest.py.
These fixtures are automatically available to all integration tests via conftest.py:
Loads the real test configuration from tests/configuration/lightspeed-stack.yaml.
def test_example(test_config: AppConfig) -> None:
assert test_config.inference.default_provider == "test-provider"Provides a fresh in-memory SQLite database engine for each test.
Provides a database session connected to the test database.
Automatically patches app.database.engine and app.database.session_local to use the test database. This applies to ALL integration tests automatically.
Creates a basic FastAPI Request object with proper HTTP scope.
Returns an AuthTuple from the real noop authentication module.
async def test_example(test_auth: AuthTuple) -> None:
user_id, username, is_system, token = test_auth
assert user_id == "00000000-0000-0000-0000-000"Creates a Request object with all Action permissions granted. Useful for tests that need to bypass authorization.
def test_example(mock_request_with_auth: Request) -> None:
assert Action.DELETE_CONVERSATION in mock_request_with_auth.state.authorized_actionsMocks the external Llama Stack client with sensible defaults:
- Returns a mock response with "This is a test response about Ansible."
- Mocks
models.list,shields.list,vector_stores.list - Mocks
conversations.createwith proper conv_ format - Can be customized in individual tests
def test_example(mock_llama_stack_client: Any) -> None:
# Customize the mock for this specific test
mock_llama_stack_client.responses.create.return_value = custom_responseHelper functions in conftest.py make it easier to create common test objects:
Create a customizable mock LLM response:
from tests.integration.conftest import create_mock_llm_response
def test_custom_response(mocker: MockerFixture) -> None:
response = create_mock_llm_response(
mocker,
content="Custom response text",
tool_calls=[...],
refusal=None, # Set to string for shield violations
input_tokens=20,
output_tokens=10,
)Parameters:
content- Response text (default: "This is a test response about Ansible.")tool_calls- Optional list of tool callsrefusal- Optional refusal message for shield violationsinput_tokens- Input token count (default: 10)output_tokens- Output token count (default: 5)
Create a mock vector store response for RAG testing:
from tests.integration.conftest import create_mock_vector_store_response
def test_rag(mocker: MockerFixture) -> None:
chunks = [
{"text": "Chunk 1", "score": 0.95, "metadata": {"source": "doc1"}},
{"text": "Chunk 2", "score": 0.85, "metadata": {"source": "doc2"}},
]
response = create_mock_vector_store_response(mocker, chunks=chunks)Create a mock tool call:
from tests.integration.conftest import create_mock_tool_call
def test_tools(mocker: MockerFixture) -> None:
tool_call = create_mock_tool_call(
mocker,
tool_name="search",
arguments={"query": "test"},
call_id="call-123",
)Use these constants for consistent test data across integration tests:
from tests.integration.conftest import (
TEST_USER_ID, # "00000000-0000-0000-0000-000"
TEST_USERNAME, # "lightspeed-user"
TEST_CONVERSATION_ID, # "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
TEST_REQUEST_ID, # "123e4567-e89b-12d3-a456-426614174000"
TEST_OTHER_USER_ID, # "11111111-1111-1111-1111-111111111111"
TEST_NON_EXISTENT_ID, # "00000000-0000-0000-0000-000000000001"
TEST_MODEL, # "test-provider/test-model"
TEST_PROVIDER, # "test-provider"
TEST_MODEL_NAME, # "test-model"
)Integration test files should be named test_<component>_integration.py:
test_query_integration.py- Tests for query endpointstest_streaming_query_integration.py- Tests for streaming querytest_conversations_v1_integration.py- Tests for v1 conversation endpoints
"""Integration tests for <component description>."""
# pylint: disable=too-many-arguments # Integration tests need many fixtures
# pylint: disable=too-many-positional-arguments # Integration tests need many fixtures
import pytest
from pytest_mock import MockerFixture
from app.endpoints.example import example_handler
from authentication.interface import AuthTuple
from configuration import AppConfig
@pytest.mark.asyncio
async def test_example_endpoint_success(
test_config: AppConfig,
mock_llama_stack_client: Any,
test_request: Request,
test_auth: AuthTuple,
) -> None:
"""Test that example endpoint returns successful response.
This integration test verifies:
- Endpoint handler integrates with configuration system
- External dependencies are properly mocked
- Response structure is correct
Parameters:
test_config: Test configuration
mock_llama_stack_client: Mocked Llama Stack client
test_request: FastAPI request
test_auth: noop authentication tuple
"""
response = await example_handler(
request=test_request,
auth=test_auth,
)
assert response is not None
# ... more assertionsIntegration tests should verify:
- Component interaction - Multiple components working together
- Real implementations - Use actual database, config, authentication
- External mocks only - Mock only external services (Llama Stack, external APIs)
- Error handling - HTTP status codes, error messages
- Data flow - Database persistence, cache updates, etc.
- Low-level implementation details (use unit tests)
- Individual function logic (use unit tests)
- Every code branch (use unit tests)
uv run pytest tests/integration/ -vuv run pytest tests/integration/endpoints/test_query_integration.py -vuv run pytest tests/integration/endpoints/test_query_integration.py::test_query_v2_endpoint_successful_response -vuv run make test-integrationuv run pytest tests/integration/ -v --tb=shortData-driven tests use @pytest.mark.parametrize to run the same test logic with different inputs. This eliminates duplicate code and makes test coverage more visible.
Benefits:
- Reduce code duplication
- Add new test cases by simply adding to the data table
- See all test scenarios at a glance
- Consistent structure across similar tests
Use parameterized tests when you have:
- Multiple similar tests that differ only in input data and expected output
- Validation tests with multiple valid/invalid scenarios
- Error handling tests with different error conditions
# Define test cases as a list
TEST_CASES = [
pytest.param(
{
"input": "value1",
"expected_result": "result1",
},
id="descriptive_test_name_1",
),
pytest.param(
{
"input": "value2",
"expected_result": "result2",
},
id="descriptive_test_name_2",
),
]
@pytest.mark.asyncio
@pytest.mark.parametrize("test_case", TEST_CASES)
async def test_example_data_driven(
test_case: dict,
# ... fixtures
) -> None:
"""Data-driven test for example functionality.
Tests multiple scenarios:
- Scenario 1 description
- Scenario 2 description
Parameters:
test_case: Dictionary containing test parameters
# ... other fixtures
"""
input_value = test_case["input"]
expected = test_case["expected_result"]
result = await function_under_test(input_value)
assert result == expected-
Use descriptive
idvalues - They appear in test outputpytest.param(..., id="attachment_unknown_type_returns_422") # Good pytest.param(..., id="test1") # Bad
-
Group related test data - Keep test cases together at module level
ATTACHMENT_TEST_CASES = [...] # Define near the test that uses it
-
Document all scenarios - List scenarios in docstring
"""Data-driven test for attachments. Tests: - Single attachment - Empty payload - Invalid type (422 error) """
-
Keep test logic simple - Use if/else only for success vs. error paths
if expected_status == 200: # Success assertions else: # Error assertions
-
Use consistent dict keys - Standardize parameter names
{"expected_status": 200, "expected_error": None} # Consistent
Always use fixtures from conftest.py instead of creating your own:
# ❌ BAD - Creating custom fixture
@pytest.fixture
def my_custom_client(mocker):
# ... duplicate code
# ✅ GOOD - Using common fixture
def test_example(mock_llama_stack_client: Any):
# Customize if needed
mock_llama_stack_client.responses.create.return_value = custom_responseUse constants from conftest.py for consistency:
# ❌ BAD - Magic strings
conversation_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# ✅ GOOD - Using constant
from tests.integration.conftest import TEST_CONVERSATION_ID
conversation_id = TEST_CONVERSATION_IDUse helper functions to reduce boilerplate:
# ❌ BAD - Manually creating mocks
mock_response = mocker.MagicMock()
mock_response.output = [...]
# ... many lines of setup
# ✅ GOOD - Using helper
from tests.integration.conftest import create_mock_llm_response
mock_response = create_mock_llm_response(mocker, content="Custom text")Test names should describe what they verify:
# ❌ BAD
def test_query_1():
# ✅ GOOD
def test_query_v2_endpoint_returns_successful_response():
def test_query_v2_endpoint_handles_connection_error():
def test_query_v2_endpoint_validates_conversation_ownership():Include what the test verifies and parameters:
@pytest.mark.asyncio
async def test_example(
test_config: AppConfig,
mock_llama_stack_client: Any,
) -> None:
"""Test that example endpoint handles errors correctly.
This integration test verifies:
- Error handling when external service fails
- Proper HTTP status code is returned
- Error message is user-friendly
Parameters:
test_config: Test configuration
mock_llama_stack_client: Mocked Llama Stack client
"""Each test should verify one specific behavior:
# ❌ BAD - Testing multiple things
def test_endpoint():
# Test success case
# Test error case
# Test edge case
# ✅ GOOD - Separate tests
def test_endpoint_success():
def test_endpoint_handles_error():
def test_endpoint_handles_edge_case():# ❌ BAD - Vague assertion
assert response
# ✅ GOOD - Specific assertions
assert response is not None
assert response.conversation_id == TEST_CONVERSATION_ID
assert "Ansible" in response.response
assert response.input_tokens == 10The framework handles most cleanup automatically via fixtures. Only add explicit cleanup if needed:
@pytest.fixture
def custom_resource():
resource = setup_resource()
yield resource
# Cleanup happens here
teardown_resource(resource)If you see RuntimeError: Database session not initialized:
- The
patch_db_sessionfixture is autouse, so this shouldn't happen - Check that you're in the
tests/integration/directory - Verify conftest.py is being loaded
If you see ModuleNotFoundError:
- Ensure you're running tests with
uv run pytest - Check that imports use absolute paths:
from app.endpoints.query import ...
If mocks aren't being applied:
- Verify you're patching the right location (where it's used, not where it's defined)
- Check that the fixture is included in test parameters
- Use
mocker.patchinstead ofunittest.mock.patch
If tests interfere with each other:
- Check that fixtures have correct scope (usually
function) - Verify cleanup is happening in fixtures
- Use
pytest --tb=short -xto stop on first failure
When adding new common functionality:
- Add to conftest.py - If it's useful across multiple test files
- Document here - Add to this README
- Add examples - Show how to use it
- Keep it simple - Don't over-engineer
When modifying existing fixtures:
- Check usage - Search for uses across all test files
- Maintain backwards compatibility - Don't break existing tests
- Update docs - Keep this README current