Skip to main content

Testing Overview

VoicePact uses a combination of unit tests, integration tests, and demo scripts to ensure reliability. The test suite focuses on:
  • SMS integration with Africa’s Talking
  • Contract generation and term extraction
  • Voice processing workflows
  • API endpoint validation

Test Structure

server/tests/
├── __init__.py
├── test_sms_demo.py              # SMS workflow demo
├── test_contract_generation.py   # Contract logic tests
└── test_africastalking_integration.py  # API integration tests

Prerequisites

Before running tests:
  1. Complete development setup (see local development guide)
  2. Configure environment variables in .env
  3. Start required services (Redis)
  4. Activate virtual environment
cd server
source venv/bin/activate

Running Tests

Basic Test Execution

# Run all tests
pytest

# Run specific test file
pytest tests/test_sms_demo.py

# Run with verbose output
pytest -v

# Run with coverage
pytest --cov=app --cov-report=html
Install pytest if not already available: pip install pytest pytest-asyncio pytest-cov

SMS Demo Script

The SMS demo script (test_sms_demo.py) provides a complete end-to-end demonstration of the VoicePact workflow:
cd server
python tests/test_sms_demo.py
This script demonstrates:
  1. ✅ Voice transcript simulation
  2. ✅ AI contract term extraction
  3. ✅ Contract generation with cryptographic signing
  4. ✅ SMS notifications to parties
  5. ✅ Payment escrow simulation
  6. ✅ USSD menu flow
  7. ✅ Delivery confirmation

Demo Script Output

🎙️ VoicePact SMS Demo
This demonstrates VoicePact functionality using SMS API
Perfect for hackathon demos when Voice API isn't available

Environment: development
AT Username: sandbox
API Key Set: Yes

VoicePact SMS Demo Starting...
==================================================

Step 1: Creating Contract from 'Voice' Transcript
Transcript: John: Grace, my maize will be ready September 20th...

Step 2: AI Contract Term Extraction
Terms extracted: Grade A Maize - KES 320,000

Step 3: Contract Generation
Contract created: AG-1234567890
Hash: a1b2c3d4e5f6g7h8...

Step 4: Sending SMS Confirmations
------------------------------
VoicePact Contract:
ID: AG-1234567890
Product: Grade A Maize (100 bags)
Value: KES 320,000

Reply YES-AG-1234567890 to confirm
Reply NO-AG-1234567890 to decline
------------------------------
SMS sent to 2 recipients

Step 5: SMS Confirmation Simulation
📲 +254711082231: YES-AG-1234567890
📲 +254711082231: YES-AG-1234567890

Step 6: Payment Escrow Simulation
Buyer pays upfront: KES 96,000

Step 7: USSD Menu Simulation (*483#)
VoicePact USSD Menu
1. View My Contracts
2. Confirm Delivery
3. Check Payments
4. Help & Support

Contract Status:
AG-1234567890...
Status: Confirmed
Amount: KES 320,000

Step 8: Delivery Confirmation Flow
Delivery SMS to buyer:
[Delivery confirmation message]

Demo Complete!
==================================================
Contract Created: AG-1234567890
Parties Notified: 2 via SMS
Payment Escrow: KES 96,000
Multi-modal Access: SMS + USSD
Crypto Security: Contract hash generated

Test Examples

SMS Integration Testing

From test_sms_demo.py:184:
import asyncio
import sys
import os

sys.path.append(os.path.dirname(os.path.dirname(__file__)))

from app.services.africastalking_client import AfricasTalkingClient
from app.core.config import get_settings

class SMSDemo:
    def __init__(self):
        self.settings = get_settings()
        self.at_client = None
        
    async def initialize(self):
        """Initialize the AT client"""
        self.at_client = AfricasTalkingClient()
        print("Africa's Talking client initialized")
    
    async def test_sms_basic(self):
        """Test basic SMS functionality"""
        test_number = "+254700000000"
        
        try:
            message = f"VoicePact SMS Test - {datetime.now().strftime('%H:%M:%S')}"
            
            if self.should_send_real_sms():
                response = await self.at_client.send_sms(
                    message=message,
                    recipients=[test_number]
                )
                print(f"SMS sent successfully: {response}")
            else:
                print(f"Would send SMS: '{message}' to {test_number}")
                print("Set real API key and phone number to test")
                
        except Exception as e:
            print(f"SMS test failed: {e}")
    
    def should_send_real_sms(self) -> bool:
        """Check if we should send real SMS"""
        return (
            self.settings.at_api_key != "your_africastalking_api_key_here" and
            self.settings.at_username != "sandbox" and
            len(self.settings.get_secret_value('at_api_key')) > 10
        )

async def main():
    demo = SMSDemo()
    await demo.initialize()
    await demo.test_sms_basic()

if __name__ == "__main__":
    asyncio.run(main())

Contract Generation Testing

Test contract term extraction and generation:
from app.services.contract_generator import ContractGenerator
from app.services.crypto_service import CryptoService

async def test_contract_generation():
    """Test contract generation workflow"""
    generator = ContractGenerator()
    crypto = CryptoService()
    
    # Sample transcript
    transcript = """
    John: Grace, my maize will be ready September 20th. 
          I can offer you 100 bags of grade A maize.
    Grace: Perfect timing John. What's your price per bag?
    John: KES 3,200 per bag. I need 30% payment upfront this time.
    Grace: Deal. So 100 bags at KES 3,200, that's KES 320,000 total.
    """
    
    # Extract terms
    terms = {
        "product": "Grade A Maize",
        "quantity": "100",
        "unit": "bags",
        "unit_price": 3200,
        "total_amount": 320000,
        "currency": "KES",
        "upfront_payment": 96000,
        "delivery_location": "Thika Road Warehouse",
        "delivery_deadline": "September 20, 2025",
        "quality_requirements": "Grade A standard"
    }
    
    # Generate contract ID and hash
    contract_id = generator.generate_contract_id("agricultural_supply")
    contract_hash = crypto.generate_contract_hash(
        f"{transcript}:{str(sorted(terms.items()))}"
    )
    
    print(f"Contract ID: {contract_id}")
    print(f"Contract Hash: {contract_hash}")
    print(f"Terms: {terms}")
    
    assert contract_id.startswith("AG-")
    assert len(contract_hash) > 0
    assert terms["total_amount"] == 320000

API Endpoint Testing

Test FastAPI endpoints:
import pytest
from httpx import AsyncClient
from server.main import app

@pytest.mark.asyncio
async def test_health_endpoint():
    """Test health check endpoint"""
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/health")
        assert response.status_code == 200
        data = response.json()
        assert "status" in data
        assert data["status"] in ["healthy", "unhealthy"]

@pytest.mark.asyncio
async def test_sms_status_endpoint():
    """Test SMS service status"""
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/v1/sms/status")
        assert response.status_code == 200
        data = response.json()
        assert data["service"] == "SMS"
        assert "service_available" in data

@pytest.mark.asyncio
async def test_voice_upload_endpoint():
    """Test voice file upload"""
    async with AsyncClient(app=app, base_url="http://test") as client:
        # Create test audio file
        files = {"file": ("test.wav", b"fake audio data", "audio/wav")}
        response = await client.post("/api/v1/voice/upload", files=files)
        
        # Should fail with fake data but endpoint should work
        assert response.status_code in [200, 400, 500]

Testing with Real APIs

SMS Testing

To test with real SMS sending:
  1. Set valid credentials in .env:
    AT_USERNAME=your_username
    AT_API_KEY=your_real_api_key
    
  2. Update test phone number in test_sms_demo.py:42:
    test_number = "+254711082231"  # Your verified number
    
  3. Run the demo:
    python tests/test_sms_demo.py
    
SMS testing with real API keys will consume credits. Use sandbox mode for development.

Voice API Testing

Voice testing requires webhook configuration:
  1. Start ngrok (see ngrok setup guide)
  2. Update webhook URL in .env
  3. Test voice endpoints via API docs at /docs

Payment Testing

Payment testing in sandbox mode:
# From test_sms_demo.py:132
try:
    payment_response = await self.at_client.mobile_checkout(
        phone_number=buyer_phone,
        amount=upfront_amount,
        currency_code=terms['currency'],
        metadata={"contract_id": contract_id, "type": "upfront"}
    )
    print(f"Payment initiated: {payment_response}")
except Exception as e:
    print(f"Payment initiation failed: {e}")
    print("This is normal in sandbox mode")
Sandbox payments will not complete but will test the integration flow.

Mocking External Services

Mock Africa’s Talking Client

import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def mock_at_client():
    """Mock Africa's Talking client"""
    client = AsyncMock()
    client.send_sms = AsyncMock(return_value={
        "SMSMessageData": {
            "Recipients": [{
                "number": "+254712345678",
                "status": "Success",
                "messageId": "test-message-id"
            }]
        }
    })
    return client

@pytest.mark.asyncio
async def test_with_mock_client(mock_at_client):
    """Test using mocked client"""
    response = await mock_at_client.send_sms(
        message="Test",
        recipients=["+254712345678"]
    )
    assert response["SMSMessageData"]["Recipients"][0]["status"] == "Success"

Mock Voice Processor

@pytest.fixture
def mock_voice_processor():
    """Mock voice processing service"""
    processor = AsyncMock()
    processor.process_voice_to_contract = AsyncMock(return_value={
        "transcript": "Sample transcript",
        "terms": {
            "product": "Grade A Maize",
            "quantity": "100",
            "total_amount": 320000
        },
        "processing_status": "completed",
        "confidence_score": 0.95
    })
    return processor

Test Configuration

Create pytest.ini in the server directory:
[pytest]
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = 
    -v
    --strict-markers
    --tb=short
    --asyncio-mode=auto
testpaths = tests
markers =
    integration: Integration tests (deselect with '-m "not integration"')
    unit: Unit tests
    slow: Slow running tests

Continuous Testing

Watch Mode

Run tests automatically on file changes:
# Install pytest-watch
pip install pytest-watch

# Run in watch mode
ptw -- -v

Pre-commit Hooks

Add testing to git hooks:
# .git/hooks/pre-commit
#!/bin/bash
cd server
source venv/bin/activate
pytest tests/ -v
if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

Coverage Reports

Generate test coverage reports:
# Generate HTML coverage report
pytest --cov=app --cov-report=html tests/

# View report
open htmlcov/index.html  # macOS
xdg-open htmlcov/index.html  # Linux

# Generate terminal report
pytest --cov=app --cov-report=term-missing tests/
Target coverage goals:
  • Unit tests: 80%+ coverage
  • Integration tests: Critical paths covered
  • API endpoints: All routes tested

Debugging Tests

# Show print statements
pytest -s

# Show print statements with verbose output
pytest -sv

PDB Debugging

import pytest

def test_example():
    result = some_function()
    
    # Drop into debugger
    import pdb; pdb.set_trace()
    
    assert result == expected
Run with:
pytest --pdb  # Drop into debugger on failures
pytest --trace  # Drop into debugger at start of each test

Common Testing Issues

Symptom: RuntimeWarning: coroutine was never awaitedSolution: Ensure async tests use @pytest.mark.asyncio:
@pytest.mark.asyncio
async def test_async_function():
    result = await some_async_function()
    assert result
Symptom: SQLite database locked errorsSolution: Use separate test database:
# In conftest.py
@pytest.fixture
def test_db():
    test_db_url = "sqlite:///./test_voicepact.db"
    # Setup test database
    yield
    # Cleanup
Symptom: ModuleNotFoundError in test filesSolution: Ensure correct path setup:
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
Symptom: Tests hang or timeout waiting for APISolution: Use shorter timeouts and mocks:
@pytest.mark.timeout(5)
async def test_with_timeout():
    # Test code
    pass

Best Practices

Test Organization

Do:
  • Group related tests in classes
  • Use descriptive test names
  • Test one thing per test
  • Use fixtures for setup/teardown
  • Mock external services
Don’t:
  • Write tests that depend on each other
  • Use real API keys in tests
  • Test implementation details
  • Leave debug statements

Test Naming

# Good test names
def test_sms_sends_to_valid_phone_number()
def test_contract_generation_with_valid_terms()
def test_payment_escrow_creation_succeeds()

# Bad test names
def test_1()
def test_sms()
def test_function()

Assertion Messages

# Good - descriptive messages
assert result.status == "success", f"Expected success but got {result.status}"
assert len(contracts) > 0, "No contracts were created"

# Bad - no context
assert result
assert len(contracts)

Next Steps

ngrok Setup

Configure webhooks for testing Voice/SMS/USSD

Local Development

Return to development setup guide

API Reference

Explore API endpoints for testing

Architecture

Understand system architecture

Build docs developers (and LLMs) love