Overview
This guide provides comprehensive testing strategies for integrating with the HopNow API, including unit tests, integration tests, and production monitoring.Testing Environment Setup
1. API Credentials
Create separate API keys for different environments:- Development: Use sandbox API keys for local development
- Testing: Dedicated test environment keys for CI/CD
- Staging: Pre-production keys that mirror production setup
- Production: Live keys with appropriate restrictions
2. Base Configuration
Copy
import os
from enum import Enum
class Environment(Enum):
DEVELOPMENT = "development"
TESTING = "testing"
STAGING = "staging"
PRODUCTION = "production"
class APIConfig:
def __init__(self, env: Environment):
self.environment = env
self.base_url = self._get_base_url()
self.api_key_id = os.getenv(f"GT_API_KEY_ID_{env.value.upper()}")
self.api_secret = os.getenv(f"GT_API_SECRET_{env.value.upper()}")
def _get_base_url(self) -> str:
urls = {
Environment.DEVELOPMENT: "https://apis-sbx.hopnow.io",
Environment.TESTING: "https://apis-sbx.hopnow.io",
Environment.STAGING: "https://apis-sbx.hopnow.io",
Environment.PRODUCTION: "https://apis.hopnow.io"
}
return urls[self.environment]
Unit Testing
1. HMAC Signature Generation
Test your signature generation logic with known test vectors:Copy
import unittest
import hmac
import hashlib
class TestHMACSignature(unittest.TestCase):
def setUp(self):
self.api_secret = "test_secret_key_123"
def test_post_request_signature(self):
"""Test HMAC signature for POST request"""
method = "POST"
url = "https://apis.hopnow.io/v1/test"
timestamp = "1640995200"
body = '{"test":true}'
expected_payload = f"{method}{url}{timestamp}{body}"
signature = self._create_signature(method, url, timestamp, body)
# Verify signature is 64 characters (SHA256 hex)
self.assertEqual(len(signature), 64)
self.assertTrue(all(c in '0123456789abcdef' for c in signature))
def test_get_request_signature(self):
"""Test HMAC signature for GET request (empty body)"""
method = "GET"
url = "https://apis.hopnow.io/v1/customers/cus_123/accounts"
timestamp = "1640995200"
body = ""
signature = self._create_signature(method, url, timestamp, body)
self.assertEqual(len(signature), 64)
def test_signature_consistency(self):
"""Test that same inputs produce same signature"""
method = "POST"
url = "https://apis.hopnow.io/v1/test"
timestamp = "1640995200"
body = '{"test":true}'
signature1 = self._create_signature(method, url, timestamp, body)
signature2 = self._create_signature(method, url, timestamp, body)
self.assertEqual(signature1, signature2)
def _create_signature(self, method, url, timestamp, body):
"""Helper to create HMAC signature"""
payload = f"{method.upper()}{url}{timestamp}{body}"
return hmac.new(
self.api_secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
if __name__ == '__main__':
unittest.main()
2. Request Construction
Test that your API client constructs requests correctly:Copy
class TestAPIClient(unittest.TestCase):
def setUp(self):
self.client = GlobalTransferClient("test_key", "test_secret")
def test_request_headers(self):
"""Test that all required headers are included"""
with patch('requests.request') as mock_request:
self.client._make_request("POST", "/v1/test", {"data": "test"})
call_args = mock_request.call_args
headers = call_args[1]['headers']
# Check all required headers are present
self.assertIn('X-API-Key', headers)
self.assertIn('X-Signature', headers)
self.assertIn('X-Timestamp', headers)
self.assertIn('Content-Type', headers)
# Verify header values
self.assertEqual(headers['Content-Type'], 'application/json')
self.assertEqual(headers['X-API-Key'], 'test_key')
def test_timestamp_freshness(self):
"""Test that timestamp is current"""
with patch('requests.request') as mock_request:
before = int(time.time())
self.client._make_request("GET", "/v1/test")
after = int(time.time())
call_args = mock_request.call_args
headers = call_args[1]['headers']
timestamp = int(headers['X-Timestamp'])
self.assertGreaterEqual(timestamp, before)
self.assertLessEqual(timestamp, after)
Integration Testing
1. Authentication Testing
Test that your authentication works against the actual API:Copy
class TestAPIAuthentication(unittest.TestCase):
def setUp(self):
# Use sandbox credentials
self.client = GlobalTransferClient(
os.getenv("GT_API_KEY_ID_TESTING"),
os.getenv("GT_API_SECRET_TESTING"),
base_url="https://apis-sbx.hopnow.io"
)
def test_authentication_success(self):
"""Test successful authentication"""
response = self.client.get("/v1/auth/test")
self.assertIn("customer_id", response)
self.assertIn("api_key_id", response)
self.assertEqual(response["message"], "Authentication successful")
def test_authentication_with_invalid_key(self):
"""Test authentication with invalid API key"""
client = GlobalTransferClient(
"invalid_key",
"invalid_secret",
base_url="https://apis-sbx.hopnow.io"
)
with self.assertRaises(AuthenticationError):
client.get("/v1/auth/test")
2. End-to-End Workflow Testing
Test complete workflows like account creation:Copy
class TestAccountWorkflow(unittest.TestCase):
def setUp(self):
self.client = GlobalTransferClient(
os.getenv("GT_API_KEY_ID_TESTING"),
os.getenv("GT_API_SECRET_TESTING"),
base_url="https://apis-sbx.hopnow.io"
)
self.customer_id = os.getenv("GT_TEST_CUSTOMER_ID")
def test_account_creation_and_retrieval(self):
"""Test creating and retrieving an account"""
# Create account
account_data = {"name": f"Test Account {int(time.time())}"}
created_account = self.client.post(
f"/v1/customers/{self.customer_id}/accounts",
account_data
)
# Verify creation response
self.assertIn("id", created_account)
self.assertEqual(created_account["name"], account_data["name"])
self.assertEqual(created_account["status"], "active")
account_id = created_account["id"]
# Retrieve account
retrieved_account = self.client.get(
f"/v1/customers/{self.customer_id}/accounts/{account_id}"
)
# Verify retrieval
self.assertEqual(retrieved_account["id"], account_id)
self.assertEqual(retrieved_account["name"], account_data["name"])
# Clean up - deactivate account
self.client.delete(
f"/v1/customers/{self.customer_id}/accounts/{account_id}"
)
Load Testing
1. Rate Limit Testing
Test your application’s behavior under rate limits:Copy
import concurrent.futures
import time
class TestRateLimits(unittest.TestCase):
def setUp(self):
self.client = GlobalTransferClient(
os.getenv("GT_API_KEY_ID_TESTING"),
os.getenv("GT_API_SECRET_TESTING"),
base_url="https://apis-sbx.hopnow.io"
)
def test_rate_limit_handling(self):
"""Test rate limit response and retry behavior"""
def make_request():
try:
return self.client.get("/v1/auth/test")
except RateLimitError as e:
return e
# Make many concurrent requests
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(make_request) for _ in range(100)]
results = [future.result() for future in futures]
# Check that some requests succeeded and rate limits were handled
successes = [r for r in results if not isinstance(r, Exception)]
rate_limits = [r for r in results if isinstance(r, RateLimitError)]
self.assertGreater(len(successes), 0, "Some requests should succeed")
self.assertGreater(len(rate_limits), 0, "Some requests should hit rate limits")
Error Handling Tests
1. Network Error Simulation
Test how your application handles network issues:Copy
from unittest.mock import patch
import requests
class TestErrorHandling(unittest.TestCase):
def setUp(self):
self.client = GlobalTransferClient("test_key", "test_secret")
@patch('requests.request')
def test_network_timeout_handling(self, mock_request):
"""Test handling of network timeouts"""
mock_request.side_effect = requests.exceptions.Timeout()
with self.assertRaises(NetworkError):
self.client.get("/v1/test")
@patch('requests.request')
def test_server_error_retry(self, mock_request):
"""Test retry behavior for server errors"""
# First call fails, second succeeds
mock_request.side_effect = [
requests.exceptions.HTTPError(response=Mock(status_code=500)),
Mock(json=lambda: {"success": True}, status_code=200)
]
result = self.client.get("/v1/test")
self.assertEqual(result["success"], True)
self.assertEqual(mock_request.call_count, 2)
Test Data Management
1. Test Account Creation
Create isolated test accounts for consistent testing:Copy
class TestDataManager:
def __init__(self, client, customer_id):
self.client = client
self.customer_id = customer_id
self.created_accounts = []
def create_test_account(self, name_suffix=""):
"""Create a test account and track for cleanup"""
account_data = {
"name": f"Test Account {int(time.time())}{name_suffix}"
}
account = self.client.post(
f"/v1/customers/{self.customer_id}/accounts",
account_data
)
self.created_accounts.append(account["id"])
return account
def cleanup(self):
"""Clean up all created test accounts"""
for account_id in self.created_accounts:
try:
self.client.delete(
f"/v1/customers/{self.customer_id}/accounts/{account_id}"
)
except Exception as e:
print(f"Failed to clean up account {account_id}: {e}")
self.created_accounts.clear()
# Usage in tests
class TestWithCleanup(unittest.TestCase):
def setUp(self):
self.client = GlobalTransferClient(...)
self.test_data = TestDataManager(self.client, "cus_test_123")
def tearDown(self):
self.test_data.cleanup()
def test_some_functionality(self):
account = self.test_data.create_test_account("_for_balance_test")
# Test logic here...
Monitoring and Alerts
1. Production Health Checks
Implement health checks that monitor your API integration:Copy
class APIHealthCheck:
def __init__(self, client):
self.client = client
def check_authentication(self):
"""Check if authentication is working"""
try:
response = self.client.get("/v1/auth/test")
return {
"status": "healthy",
"response_time": response.get("response_time"),
"message": "Authentication successful"
}
except Exception as e:
return {
"status": "unhealthy",
"error": str(e),
"message": "Authentication failed"
}
def check_api_availability(self):
"""Check if API is responding"""
import time
start_time = time.time()
try:
self.client.get("/v1/auth/test")
response_time = (time.time() - start_time) * 1000
return {
"status": "healthy" if response_time < 5000 else "degraded",
"response_time_ms": response_time,
"message": f"API responding in {response_time:.0f}ms"
}
except Exception as e:
return {
"status": "unhealthy",
"response_time_ms": (time.time() - start_time) * 1000,
"error": str(e),
"message": "API not responding"
}
def full_health_check(self):
"""Complete health check"""
return {
"authentication": self.check_authentication(),
"availability": self.check_api_availability(),
"timestamp": time.time()
}
2. Error Monitoring
Set up monitoring for authentication and API errors:Copy
import logging
from datetime import datetime, timedelta
class APIErrorMonitor:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.error_counts = {}
def log_authentication_error(self, error):
"""Log authentication errors"""
self.logger.error(
"API authentication failed",
extra={
"error_type": "authentication_error",
"error_code": getattr(error, 'code', 'unknown'),
"timestamp": datetime.utcnow().isoformat()
}
)
self._increment_error_count("authentication_error")
def log_rate_limit_error(self, error):
"""Log rate limit errors"""
self.logger.warning(
"API rate limit exceeded",
extra={
"error_type": "rate_limit_error",
"retry_after": getattr(error, 'retry_after', None),
"timestamp": datetime.utcnow().isoformat()
}
)
self._increment_error_count("rate_limit_error")
def _increment_error_count(self, error_type):
"""Track error counts for alerting"""
now = datetime.utcnow()
hour_key = now.replace(minute=0, second=0, microsecond=0)
if hour_key not in self.error_counts:
self.error_counts[hour_key] = {}
if error_type not in self.error_counts[hour_key]:
self.error_counts[hour_key][error_type] = 0
self.error_counts[hour_key][error_type] += 1
# Alert if too many errors in the last hour
if self.error_counts[hour_key][error_type] > 10:
self._send_alert(error_type, self.error_counts[hour_key][error_type])
def _send_alert(self, error_type, count):
"""Send alert for high error rates"""
self.logger.critical(
f"High error rate detected: {count} {error_type} errors in the last hour"
)
# Integrate with your alerting system (PagerDuty, Slack, etc.)
Continuous Integration
1. CI/CD Pipeline Testing
Example GitHub Actions workflow for API testing:Copy
name: API Integration Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run unit tests
run: pytest tests/unit/ -v
- name: Run integration tests
env:
GT_API_KEY_ID_TESTING: ${{ secrets.GT_API_KEY_ID_TESTING }}
GT_API_SECRET_TESTING: ${{ secrets.GT_API_SECRET_TESTING }}
GT_TEST_CUSTOMER_ID: ${{ secrets.GT_TEST_CUSTOMER_ID }}
run: pytest tests/integration/ -v
- name: Run load tests
run: pytest tests/load/ -v --maxfail=1
- name: Generate coverage report
run: |
coverage run -m pytest
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
Best Practices Summary
1. Test Organization
- Separate unit, integration, and load tests
- Use consistent test data management
- Implement proper cleanup in teardown methods
2. Environment Management
- Use separate API keys for each environment
- Never commit credentials to version control
- Test against sandbox environments first
3. Error Testing
- Test all error scenarios (network, authentication, rate limits)
- Verify retry logic and backoff strategies
- Monitor error rates in production
4. Performance Testing
- Test rate limit handling
- Measure and monitor response times
- Load test critical workflows
5. Monitoring
- Implement health checks for production
- Set up alerting for high error rates
- Log structured data for analysis
Need help setting up testing for your specific use case? Contact our support team for assistance.