Backend Testing
This guide covers the comprehensive backend testing framework used in Nexent, including API testing, service layer testing, and utility function testing.
Test Structure
The backend tests are organized in the following structure:
test/backend/
├── app/ # API endpoint tests
│ ├── test_agent_app.py
│ ├── test_base_app.py
│ ├── test_config_sync_app.py
│ ├── test_conversation_management_app.py
│ ├── test_data_process_app.py
│ ├── test_elasticsearch_app.py
│ ├── test_file_management_app.py
│ ├── test_image_app.py
│ ├── test_knowledge_app.py
│ ├── test_knowledge_summary_app.py
│ ├── test_me_model_managment_app.py
│ ├── test_model_managment_app.py
│ ├── test_prompt_app.py
│ └── test_remote_mcp_app.py
├── services/ # Service layer tests
│ ├── test_agent_service.py
│ ├── test_conversation_management_service.py
│ ├── test_data_process_service.py
│ ├── test_elasticsearch_service.py
│ ├── test_file_management_service.py
│ ├── test_image_service.py
│ ├── test_knowledge_service.py
│ ├── test_knowledge_summary_service.py
│ ├── test_model_management_service.py
│ ├── test_prompt_service.py
│ └── test_remote_mcp_service.py
├── utils/ # Utility function tests
│ ├── test_langchain_utils.py
│ └── test_prompt_template_utils.py
└── run_all_test.py # Backend test runner
Running Backend Tests
Complete Backend Test Suite
# From project root
python test/backend/run_all_test.py
# From test/backend directory
cd test/backend
python run_all_test.py
Individual Test Categories
# Run all API tests
python -m pytest test/backend/app/ -v
# Run all service tests
python -m pytest test/backend/services/ -v
# Run all utility tests
python -m pytest test/backend/utils/ -v
Specific Test Files
# Run specific API test
python -m pytest test/backend/app/test_agent_app.py -v
# Run specific service test
python -m pytest test/backend/services/test_agent_service.py -v
# Run specific utility test
python -m pytest test/backend/utils/test_langchain_utils.py -v
API Testing
API tests use FastAPI's TestClient to simulate HTTP requests without running an actual server.
Test Setup Pattern
import os
import sys
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from fastapi import FastAPI
# Dynamically determine the backend path
current_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.abspath(os.path.join(current_dir, "../../../backend"))
sys.path.append(backend_dir)
# Setup patches for dependencies before importing modules
patches = [
patch('botocore.client.BaseClient._make_api_call', return_value={}),
patch('backend.database.client.MinioClient', MagicMock()),
patch('backend.database.client.db_client', MagicMock()),
patch('backend.utils.auth_utils.get_current_user_id',
MagicMock(return_value=('test_user', 'test_tenant'))),
patch('httpx.AsyncClient', MagicMock())
]
# Start all patches
for p in patches:
p.start()
# Import modules after applying patches
from backend.apps.agent_app import router
# Create test app
app = FastAPI()
app.include_router(router)
client = TestClient(app)
API Test Example
class TestAgentApp(unittest.TestCase):
def setUp(self):
# Setup test client and common mocks
pass
def test_create_agent_success(self):
"""Test successful agent creation"""
# Setup
agent_data = {
"name": "Test Agent",
"description": "A test agent",
"system_prompt": "You are a test agent"
}
# Execute
response = client.post("/agents", json=agent_data)
# Assert
self.assertEqual(response.status_code, 200)
self.assertIn("id", response.json())
self.assertEqual(response.json()["name"], "Test Agent")
def test_create_agent_invalid_data(self):
"""Test agent creation with invalid data"""
# Setup
invalid_data = {"name": ""} # Missing required fields
# Execute
response = client.post("/agents", json=invalid_data)
# Assert
self.assertEqual(response.status_code, 422) # Validation error
Service Layer Testing
Service layer tests focus on business logic and data processing without HTTP overhead.
Service Test Pattern
class TestAgentService(unittest.TestCase):
@patch("backend.database.agent_db.save_agent")
@patch("backend.utils.auth_utils.get_current_user_id")
async def test_create_agent_success(self, mock_get_user, mock_save_agent):
# Setup
mock_get_user.return_value = ("user123", "tenant456")
mock_save_agent.return_value = {"id": 1, "name": "Test Agent"}
# Execute
result = await create_agent(
name="Test Agent",
description="A test agent",
system_prompt="You are a test agent"
)
# Assert
mock_save_agent.assert_called_once()
self.assertEqual(result["name"], "Test Agent")
self.assertIn("id", result)
Mocking Database Operations
@patch("backend.database.agent_db.query_agent_by_id")
@patch("backend.database.agent_db.update_agent")
async def test_update_agent_success(self, mock_update, mock_query):
# Setup
mock_query.return_value = {"id": 1, "name": "Old Name"}
mock_update.return_value = {"id": 1, "name": "New Name"}
# Execute
result = await update_agent(agent_id=1, name="New Name")
# Assert
mock_update.assert_called_once_with(agent_id=1, name="New Name")
self.assertEqual(result["name"], "New Name")
Utility Function Testing
Utility functions are tested in isolation with mocked dependencies.
Utility Test Example
class TestLangchainUtils(unittest.TestCase):
@patch("langchain.llms.openai.OpenAI")
def test_create_llm_instance(self, mock_openai):
# Setup
mock_openai.return_value = MagicMock()
# Execute
llm = create_llm_instance(model_name="gpt-3.5-turbo")
# Assert
mock_openai.assert_called_once()
self.assertIsNotNone(llm)
Testing Asynchronous Code
Backend tests handle both synchronous and asynchronous code:
Async Test Pattern
class TestAsyncService(unittest.TestCase):
@patch("backend.database.agent_db.async_query")
async def test_async_operation(self, mock_async_query):
# Setup
mock_async_query.return_value = {"result": "success"}
# Execute
result = await async_operation()
# Assert
self.assertEqual(result["result"], "success")
mock_async_query.assert_called_once()
Error Handling Tests
Comprehensive error handling is tested:
def test_api_error_handling(self):
"""Test API error responses"""
# Setup - mock service to raise exception
with patch('backend.services.agent_service.create_agent') as mock_service:
mock_service.side_effect = Exception("Database error")
# Execute
response = client.post("/agents", json={"name": "Test"})
# Assert
self.assertEqual(response.status_code, 500)
self.assertIn("error", response.json())
Authentication and Authorization Tests
Security-related functionality is thoroughly tested:
def test_authentication_required(self):
"""Test that endpoints require authentication"""
# Execute without auth header
response = client.get("/agents")
# Assert
self.assertEqual(response.status_code, 401)
def test_tenant_isolation(self):
"""Test that users can only access their tenant's data"""
# Setup - mock auth to return different tenant
with patch('backend.utils.auth_utils.get_current_user_id') as mock_auth:
mock_auth.return_value = ("user1", "tenant1")
# Execute
response = client.get("/agents")
# Assert - verify tenant filtering is applied
# This would check that the service layer filters by tenant
Coverage Analysis
Backend tests generate detailed coverage reports:
Coverage Commands
# Generate coverage report
python -m pytest test/backend/ --cov=backend --cov-report=html --cov-report=xml
# View coverage in terminal
python -m pytest test/backend/ --cov=backend --cov-report=term-missing
Coverage Targets
- API Endpoints: 90%+ coverage
- Service Layer: 85%+ coverage
- Utility Functions: 80%+ coverage
- Error Handling: 100% coverage for critical paths
Test Data Management
Fixtures and Test Data
class TestWithFixtures(unittest.TestCase):
def setUp(self):
"""Set up test data and mocks"""
self.test_agent = {
"id": 1,
"name": "Test Agent",
"description": "A test agent",
"system_prompt": "You are a test agent"
}
self.test_user = ("user123", "tenant456")
def tearDown(self):
"""Clean up after tests"""
# Reset any global state if needed
pass
Performance Testing
Backend tests include performance considerations:
def test_api_response_time(self):
"""Test that API responses are within acceptable time limits"""
import time
start_time = time.time()
response = client.get("/agents")
end_time = time.time()
# Assert response time is under 100ms
self.assertLess(end_time - start_time, 0.1)
self.assertEqual(response.status_code, 200)
Best Practices for Backend Testing
- Mock External Dependencies: Always mock database, external APIs, and services
- Test Both Success and Failure: Cover all possible code paths
- Use Descriptive Test Names: Make it clear what each test validates
- Keep Tests Independent: Each test should run in isolation
- Test Edge Cases: Include boundary conditions and error scenarios
- Maintain Test Data: Use consistent, realistic test data
- Document Complex Tests: Add comments for complex test scenarios
- Regular Coverage Reviews: Monitor and improve coverage over time
This comprehensive backend testing framework ensures that all backend functionality is thoroughly validated before deployment, maintaining high code quality and reliability.