Testing Carrier Integrations
Testing is MANDATORY and follows strict patterns in Karrio. Every integration must pass comprehensive tests before being considered complete. This page covers the exact testing requirements and patterns that must be followed.
🚨 Critical Testing Rules
ABSOLUTE REQUIREMENTS (Non-Negotiable)
-
Use Python’s
unittest
Framework Only- Never use pytest, nose, or any other testing framework
- All tests must inherit from
unittest.TestCase
- Use
python -m unittest discover
to run tests
-
Follow Exact Naming Patterns
- Test files:
test_[feature].py
(e.g.,test_rate.py
,test_shipment.py
) - Test classes:
Test[CarrierName][Feature]
(e.g.,TestDHLExpressRating
) - Test methods: Exactly 4 methods per feature with specific names
- Test files:
-
Mandatory Test Structure
- Every feature requires exactly 4 test methods
- Include debug print statements before all assertions
- Use
assertListEqual
with full dict data structures - Mock all HTTP requests - never make real API calls
-
Test Data Pattern
- Each test file must end with exactly these data structures
- Only adapt content to carrier API, never change structure
Test Class Structure
Complete Test Class Template
1"""[Carrier Name] carrier [feature] tests.""" 2 3import unittest 4from unittest.mock import patch, ANY 5from .fixture import gateway 6import logging 7import karrio.sdk as karrio 8import karrio.lib as lib 9import karrio.core.models as models 10 11logger = logging.getLogger(__name__) 12 13class Test[CompactCarrierName][Feature](unittest.TestCase): 14 def setUp(self): 15 self.maxDiff = None 16 self.[Feature]Request = models.[Feature]Request(**[Feature]Payload) 17 18 def test_create_[feature]_request(self): 19 """Test request transformation from Karrio to carrier format.""" 20 request = gateway.mapper.create_[feature]_request(self.[Feature]Request) 21 print(f"Generated request: {lib.to_dict(request.serialize())}") 22 self.assertEqual(lib.to_dict(request.serialize()), [Feature]Request) 23 24 def test_[action_verb](self): 25 """Test HTTP endpoint is called correctly.""" 26 with patch("karrio.mappers.[carrier_id].proxy.lib.request") as mock: 27 mock.return_value = "{}" # Use "<r></r>" for XML APIs 28 karrio.[Feature].[action](self.[Feature]Request).from_(gateway) 29 print(f"Called URL: {mock.call_args[1]['url']}") 30 self.assertEqual( 31 mock.call_args[1]["url"], 32 f"{gateway.settings.server_url}/[endpoint]" 33 ) 34 35 def test_parse_[feature]_response(self): 36 """Test successful response parsing.""" 37 with patch("karrio.mappers.[carrier_id].proxy.lib.request") as mock: 38 mock.return_value = [Feature]Response 39 parsed_response = ( 40 karrio.[Feature].[action](self.[Feature]Request) 41 .from_(gateway) 42 .parse() 43 ) 44 print(f"Parsed response: {lib.to_dict(parsed_response)}") 45 self.assertListEqual(lib.to_dict(parsed_response), Parsed[Feature]Response) 46 47 def test_parse_error_response(self): 48 """Test error response handling.""" 49 with patch("karrio.mappers.[carrier_id].proxy.lib.request") as mock: 50 mock.return_value = ErrorResponse 51 parsed_response = ( 52 karrio.[Feature].[action](self.[Feature]Request) 53 .from_(gateway) 54 .parse() 55 ) 56 print(f"Error response: {lib.to_dict(parsed_response)}") 57 self.assertListEqual(lib.to_dict(parsed_response), ParsedErrorResponse) 58 59if __name__ == "__main__": 60 unittest.main()
Required Test Methods by Feature
Feature | Test Method Names (EXACT) |
---|---|
Rating | test_create_rate_request , test_get_rates , test_parse_rate_response , test_parse_error_response |
Shipment | test_create_shipment_request , test_create_shipment , test_parse_shipment_response , test_parse_error_response |
Tracking | test_create_tracking_request , test_get_tracking , test_parse_tracking_response , test_parse_error_response |
Pickup | test_create_pickup_request , test_schedule_pickup , test_parse_pickup_response , test_parse_error_response |
Address | test_create_address_validation_request , test_validate_address , test_parse_address_validation_response , test_parse_error_response |
Mandatory Test Data Structure
CRITICAL: Every test file must end with exactly these data structures:
1if __name__ == "__main__": 2 unittest.main() 3 4# 1. KARRIO INPUT PAYLOAD (standardized format) 5[Feature]Payload = { 6 "shipper": { 7 "address_line1": "123 Test Street", 8 "city": "Test City", 9 "postal_code": "12345", 10 "country_code": "US", 11 "state_code": "CA", 12 "person_name": "Test Person", 13 "company_name": "Test Company", 14 "phone_number": "1234567890", 15 "email": "test@example.com" 16 }, 17 "recipient": { 18 "address_line1": "456 Test Avenue", 19 "city": "Test City", 20 "postal_code": "67890", 21 "country_code": "US", 22 "state_code": "NY", 23 "person_name": "Test Recipient", 24 "company_name": "Test Recipient Company", 25 "phone_number": "9876543210", 26 "email": "recipient@example.com" 27 }, 28 "parcels": [ 29 { 30 "weight": 10.0, 31 "width": 10.0, 32 "height": 10.0, 33 "length": 10.0, 34 "weight_unit": "KG", 35 "dimension_unit": "CM", 36 "packaging_type": "BOX" 37 } 38 ] 39} 40 41# 2. CARRIER REQUEST FORMAT (adapt to carrier API) 42[Feature]Request = { 43 # Adapt this structure to match your carrier's API format 44 # For JSON APIs with camelCase: 45 "shipper": { 46 "addressLine1": "123 Test Street", 47 "city": "Test City", 48 "postalCode": "12345", 49 "countryCode": "US" 50 }, 51 # For XML APIs or snake_case, adjust accordingly 52} 53 54# 3. CARRIER RESPONSE MOCK (actual carrier format) 55[Feature]Response = """{ 56 "rates": [ 57 { 58 "serviceCode": "express", 59 "serviceName": "Express Service", 60 "totalCharge": 25.99, 61 "currency": "USD", 62 "transitDays": 2 63 } 64 ] 65}""" 66 67# 4. ERROR RESPONSE MOCK 68ErrorResponse = """{ 69 "error": { 70 "code": "rate_error", 71 "message": "Unable to get rates", 72 "details": "Invalid address provided" 73 } 74}""" 75 76# 5. PARSED SUCCESS RESPONSE (Karrio format) 77Parsed[Feature]Response = [ 78 [ 79 { 80 "carrier_id": "[carrier_id]", 81 "carrier_name": "[carrier_id]", 82 "service": "express", 83 "currency": "USD", 84 "total_charge": 25.99, 85 "transit_days": 2, 86 "meta": { 87 "service_name": "Express Service" 88 } 89 } 90 ], 91 [] # Empty errors array 92] 93 94# 6. PARSED ERROR RESPONSE (Karrio format) 95ParsedErrorResponse = [ 96 [], # Empty success data 97 [ 98 { 99 "carrier_id": "[carrier_id]", 100 "carrier_name": "[carrier_id]", 101 "code": "rate_error", 102 "message": "Unable to get rates", 103 "details": { 104 "details": "Invalid address provided" 105 } 106 } 107 ] 108]
Test Fixture Setup
Every integration needs a fixture.py
file:
1"""[Carrier Name] carrier tests fixtures.""" 2 3import karrio.sdk as karrio 4 5gateway = karrio.gateway["[carrier_id]"].create( 6 dict( 7 id="123456789", 8 test_mode=True, 9 carrier_id="[carrier_id]", 10 account_number="123456789", 11 api_key="TEST_API_KEY", # For JSON APIs 12 # username="username", # For XML APIs 13 # password="password", # For XML APIs 14 ) 15)
Response Format Patterns
Success Response Pattern
1[ 2 [data_objects], # List of successful results 3 [] # Empty errors list 4]
Error Response Pattern
1[ 2 [], # Empty or partial data 3 [error_objects] # List of error Message objects 4]
Mixed Response Pattern
1[ 2 [partial_data], # Some successful data 3 [warning_objects] # List of warnings/non-fatal errors 4]
Testing Checklist
Phase 1: Test Structure Validation
- Test files named correctly:
test_rate.py
,test_shipment.py
, etc. - Test classes named correctly:
Test[CarrierName][Feature]
- All 4 test methods present for each feature
- Debug print statements before all assertions
- Proper imports from fixture and required modules
Phase 2: Test Data Validation
- All 6 data structures present at end of each test file
- Realistic test data that matches carrier API requirements
- Mock responses in actual carrier API format
- Expected responses in correct Karrio format
- Error scenarios properly mocked and tested
Phase 3: Test Execution
- All tests pass:
python -m unittest discover -v -f [path]/tests
- No real API calls: All HTTP requests properly mocked
- Debug output present: Print statements show actual vs expected data
- Assertions use correct methods:
assertEqual
,assertListEqual
Phase 4: Integration Validation
- SDK tests still pass:
./bin/run-sdk-tests
succeeds - Plugin registration: Tests verify plugin appears correctly
- Schema imports: Generated types import without errors
- Error handling: Both success and error paths tested
Running Tests
Development Testing
Test single feature1python -m unittest tests.[carrier_name].test_rate.Test[CarrierName]Rating -v 2 3# Test entire carrier 4python -m unittest discover -v -f modules/connectors/[carrier_name]/tests 5# OR for hub carriers 6python -m unittest discover -v -f community/plugins/[carrier_name]/tests 7 8# Check specific test output 9python -m unittest tests.[carrier_name].test_rate.Test[CarrierName]Rating.test_parse_rate_response -v
Integration Testing
MANDATORY: Ensure all SDK tests still pass1source ./bin/activate-env && ./bin/run-sdk-tests 2 3# Verify plugin registration 4./bin/cli plugins list | grep [carrier_name] 5./bin/cli plugins show [carrier_name]
Common Testing Mistakes
❌ Wrong Patterns
- Using pytest instead of unittest
- Missing debug print statements
- Using
assert
instead ofself.assertXXX
- Making real HTTP requests
- Wrong test method names
- Incorrect data structure format
✅ Correct Patterns
- Always use
unittest.TestCase
- Include
print()
statements before assertions - Use
self.assertListEqual
for response parsing - Mock all HTTP calls with
@patch
- Follow exact naming conventions
- Use the 6-structure data pattern
Test Data Best Practices
- Use Realistic Data: Test data should resemble real shipping scenarios
- Cover Edge Cases: Test with various package sizes, addresses, services
- Mock Actual Responses: Use real carrier API response formats
- Test Both Paths: Success and error scenarios are equally important
- Verify Transformations: Ensure data is correctly transformed between formats
- Use Generated Types: Verify your code uses generated schema classes
Debugging Failed Tests
When Tests Fail
- Check Debug Prints: Look at the printed request/response data
- Verify Mock Data: Ensure mock responses match carrier API format
- Validate Transformations: Check that mapping functions work correctly
- Test Individual Components: Isolate provider functions for testing
- Regenerate Schemas: Ensure generated types are up-to-date
Common Debug Patterns
Add more detailed debugging1def test_parse_rate_response(self): 2 with patch("karrio.mappers.[carrier_id].proxy.lib.request") as mock: 3 mock.return_value = RateResponse 4 parsed_response = karrio.Rating.fetch(self.RateRequest).from_(gateway).parse() 5 6 print(f"Mock response: {RateResponse}") 7 print(f"Parsed response: {lib.to_dict(parsed_response)}") 8 print(f"Expected response: {ParsedRateResponse}") 9 10 self.assertListEqual(lib.to_dict(parsed_response), ParsedRateResponse)
Success Criteria
Your integration testing is complete when:
- All carrier tests pass with proper debug output
- All SDK tests still pass without regressions
- Plugin is registered and appears in CLI tools
- Schema imports work without errors
- Both success and error paths are tested
- All test patterns followed exactly as specified
Remember: Testing is not optional. It’s the primary way Karrio ensures consistency across 50+ carrier integrations.