Testing a body measurement API integration has a specific challenge: the API is an external service with real costs per request. Running the full test suite against the live API on every commit is expensive and creates fragile tests that fail when the network is slow or the service has an incident.
The solution is a layered testing strategy: unit tests run against mocked responses, integration tests hit the real API in limited scenarios, and property-based tests verify the logic that maps API outputs to size recommendations.
Layer 1: Unit tests with mocked API responses
Unit tests should run instantly, offline, and for free. Mock the HTTP client to return pre-recorded API responses.
# tests/test_sizing_service.py
import pytest
from unittest.mock import patch, MagicMock
import json
from pathlib import Path
# Fixture: pre-recorded API response for a specific input (female, 168 cm, 62 kg)
# Matches the actual body_dimensions dict structure returned by the API
MOCK_RESPONSE_FEMALE_168_62 = {
"header": {"status": "success", "calculation_model_used": "ADULT"},
"body_dimensions": {
"chest_circumference": {
"label": "Chest Circumference",
"value": 924.0,
"unit": "mm",
"type": "FLESH",
"confidence_score": 78,
"range_95": [854.0, 994.0],
"iso_code": None,
"biological_limit_status": "OK"
},
"waist_circumference_natural": {
"label": "Waist Circumference (Natural)",
"value": 726.0,
"unit": "mm",
"type": "FLESH",
"confidence_score": 76,
"range_95": [626.0, 826.0],
"iso_code": None,
"biological_limit_status": "OK"
},
"hip_circumference": {
"label": "Hip Circumference",
"value": 965.0,
"unit": "mm",
"type": "FLESH",
"confidence_score": 78,
"range_95": [885.0, 1045.0],
"iso_code": None,
"biological_limit_status": "OK"
}
},
"system_info": {"api_version": "1.4.0", "model_version": "4.0", "computation_time_ms": 12}
}
@pytest.fixture
def mock_api_response():
"""Fixture providing a mock response for 168cm/62kg female."""
return MOCK_RESPONSE_FEMALE_168_62
def test_extract_dimensions_from_response(mock_api_response):
"""Test that dimension extraction handles the API response format correctly."""
from sizing_service import extract_dimensions
dims = extract_dimensions(mock_api_response)
assert "chest_circumference" in dims
assert dims["chest_circumference"]["value"] == 924.0
assert dims["chest_circumference"]["confidence_score"] == 78
assert dims["chest_circumference"]["range_95"][0] == 854.0
def test_size_recommendation_from_dimensions(mock_api_response):
"""Test size chart matching with known inputs."""
from sizing_service import extract_dimensions, recommend_size
dims = extract_dimensions(mock_api_response)
# Brand A women's tops: M covers chest 901–960mm
recommendation = recommend_size(
chest_mm=dims["chest_circumference"]["value"],
waist_mm=dims["waist_circumference_natural"]["value"],
hip_mm=dims["hip_circumference"]["value"],
chart_key="brand_a_womens_top"
)
assert recommendation["recommended_size"] == "M"
assert recommendation["confidence"] in ("HIGH", "MEDIUM")
@patch("sizing_service.requests.post")
def test_api_call_uses_mm_for_height(mock_post, mock_api_response):
"""
Critical: body_height must be in mm, not cm.
Test that the service multiplies height_cm × 10 before calling the API.
"""
mock_response = MagicMock()
mock_response.json.return_value = mock_api_response
mock_response.status_code = 200
mock_post.return_value = mock_response
from sizing_service import get_prediction
get_prediction(gender="female", height_cm=168, weight_kg=62, region="GLOBAL")
# Verify the API was called with height in mm
call_args = mock_post.call_args
body = call_args.kwargs.get("json") or call_args.args[1]
anchors = body["input_data"]["anchors"]
assert anchors["body_height"] == 1680, (
f"body_height must be in mm (1680), got {anchors['body_height']}. "
"Did you forget to multiply height_cm × 10?"
)
@patch("sizing_service.requests.post")
def test_api_call_passes_region(mock_post, mock_api_response):
"""Test that the region parameter is passed to both input and output settings."""
mock_response = MagicMock()
mock_response.json.return_value = mock_api_response
mock_response.status_code = 200
mock_post.return_value = mock_response
from sizing_service import get_prediction
get_prediction(gender="male", height_cm=180, weight_kg=80, region="ASIA_PACIFIC")
body = mock_post.call_args.kwargs.get("json") or mock_post.call_args.args[1]
assert body["input_data"]["subject"]["input_origin_region"] == "ASIA_PACIFIC"
assert body["output_settings"]["calculation"]["target_region"] == "ASIA_PACIFIC"
@patch("sizing_service.requests.post")
def test_api_error_handling(mock_post):
"""Test graceful handling of API errors."""
mock_response = MagicMock()
mock_response.status_code = 429
mock_response.json.return_value = {"error": "Rate limit exceeded"}
mock_post.return_value = mock_response
from sizing_service import get_prediction
result = get_prediction(gender="female", height_cm=168, weight_kg=62)
# Should return a fallback, not raise an exception
assert result.get("fallback") is True or result.get("error") is not None
Layer 2: Test fixtures from recorded API responses
Record real API responses once and use them as fixtures. This gives you realistic test data without hitting the API on every run.
# scripts/record_fixtures.py
"""Run once to record API responses for test fixtures. Not part of CI."""
import json
import requests
from pathlib import Path
FIXTURES_DIR = Path("tests/fixtures")
FIXTURES_DIR.mkdir(exist_ok=True)
TEST_CASES = [
{"name": "female_168_62_global", "gender": "female", "height_cm": 168, "weight_kg": 62, "region": "GLOBAL"},
{"name": "male_180_80_global", "gender": "male", "height_cm": 180, "weight_kg": 80, "region": "GLOBAL"},
{"name": "female_155_50_asia", "gender": "female", "height_cm": 155, "weight_kg": 50, "region": "ASIA_PACIFIC"},
{"name": "male_195_100_europe", "gender": "male", "height_cm": 195, "weight_kg": 100, "region": "EUROPE"},
# Edge cases
{"name": "female_145_45_global", "gender": "female", "height_cm": 145, "weight_kg": 45, "region": "GLOBAL"},
{"name": "male_210_110_global", "gender": "male", "height_cm": 210, "weight_kg": 110, "region": "GLOBAL"},
]
for case in TEST_CASES:
response = requests.post(
"https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
json={
"input_data": {
"input_unit_system": "metric",
"subject": {"gender": case["gender"], "input_origin_region": case["region"]},
"anchors": {"body_height": int(case["height_cm"] * 10), "body_mass": case["weight_kg"]}
},
"output_settings": {
"calculation": {"target_region": case["region"], "body_build_type": "CIVILIAN"},
"requested_dimensions": {"bundle": "TORSO"},
"output_format": {"include_range_95": True, "confidence_score_threshold": 50}
}
},
headers={
"X-RapidAPI-Key": "YOUR_API_KEY",
"X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
}
)
fixture_path = FIXTURES_DIR / f"{case['name']}.json"
fixture_path.write_text(json.dumps(response.json(), indent=2))
print(f"Saved fixture: {fixture_path}")
Layer 3: Property-based tests for size chart mapping
Property-based testing (using Hypothesis) verifies that your size chart mapping algorithm holds invariants across all possible inputs, not just the ones you thought to test manually.
# tests/test_size_mapping_properties.py
from hypothesis import given, strategies as st, settings
import pytest
SIZE_ORDER = ["XS", "S", "M", "L", "XL", "XXL"]
@given(
chest_mm=st.integers(min_value=700, max_value=1500),
waist_mm=st.integers(min_value=550, max_value=1400),
hip_mm=st.integers(min_value=700, max_value=1600)
)
@settings(max_examples=500)
def test_size_recommendation_always_returns_valid_size(chest_mm, waist_mm, hip_mm):
"""
Property: for any valid body measurement input, recommend_size
always returns a valid size label and a confidence level.
"""
from sizing_service import recommend_size
result = recommend_size(
chest_mm=chest_mm,
waist_mm=waist_mm,
hip_mm=hip_mm,
chart_key="brand_a_womens_top"
)
assert result["recommended_size"] in SIZE_ORDER
assert result["confidence"] in ("HIGH", "MEDIUM", "LOW")
@given(
chest_mm=st.integers(min_value=700, max_value=1500)
)
@settings(max_examples=200)
def test_size_recommendation_monotonic_with_chest(chest_mm):
"""
Property: larger chest measurements should recommend the same or larger size.
This tests for monotonicity — a key correctness invariant.
"""
from sizing_service import recommend_size
result_base = recommend_size(chest_mm=chest_mm, waist_mm=750, hip_mm=950, chart_key="brand_a_womens_top")
result_larger = recommend_size(chest_mm=chest_mm + 50, waist_mm=750, hip_mm=950, chart_key="brand_a_womens_top")
base_idx = SIZE_ORDER.index(result_base["recommended_size"])
larger_idx = SIZE_ORDER.index(result_larger["recommended_size"])
# A 50mm larger chest should never recommend a smaller size
assert larger_idx >= base_idx, (
f"chest={chest_mm}: recommended {result_base['recommended_size']} "
f"but chest={chest_mm + 50}: recommended {result_larger['recommended_size']} (smaller)"
)
@given(
size=st.sampled_from(SIZE_ORDER),
chest_center_mm=st.integers(min_value=750, max_value=1200)
)
def test_recommendation_returns_expected_size_when_centered(size, chest_center_mm):
"""
Property: if the chest measurement is at the center of a size's range,
that size should be recommended with HIGH confidence.
"""
from sizing_service import recommend_size, BRAND_CHART
chart = BRAND_CHART.get("brand_a_womens_top", [])
target_entry = next((e for e in chart if e.label == size), None)
if target_entry is None or target_entry.chest_min_mm is None:
return # Skip if no chart entry for this size
center_mm = (target_entry.chest_min_mm + target_entry.chest_max_mm) // 2
result = recommend_size(
chest_mm=center_mm,
waist_mm=750,
hip_mm=950,
chart_key="brand_a_womens_top"
)
assert result["recommended_size"] == size
assert result["confidence"] == "HIGH"
Layer 4: Integration tests (selective, against live API)
Run integration tests only in specific CI scenarios — not on every pull request, but on release branches or nightly.
# tests/integration/test_live_api.py
import pytest
import requests
import os
LIVE_API_KEY = os.environ.get("PREDICTION_API_KEY")
@pytest.mark.integration
@pytest.mark.skipif(not LIVE_API_KEY, reason="PREDICTION_API_KEY not set")
class TestLiveAPI:
def test_basic_prediction_returns_dimensions(self):
"""Smoke test: the API returns predicted dimensions for valid inputs."""
response = requests.post(
"https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
json={
"input_data": {
"input_unit_system": "metric",
"subject": {"gender": "female", "input_origin_region": "GLOBAL"},
"anchors": {"body_height": 1680, "body_mass": 62}
},
"output_settings": {
"calculation": {"target_region": "GLOBAL", "body_build_type": "CIVILIAN"},
"requested_dimensions": {"bundle": "TORSO"},
"output_format": {"include_range_95": True}
}
},
headers={
"X-RapidAPI-Key": LIVE_API_KEY,
"X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
},
timeout=10
)
assert response.status_code == 200
data = response.json()
assert "body_dimensions" in data
assert len(data["body_dimensions"]) > 0
# Verify a specific dimension is in plausible range
chest_dim = data["body_dimensions"].get("chest_circumference")
assert chest_dim is not None
assert 750 <= chest_dim["value"] <= 1200, f"Unexpected chest value: {chest_dim['value']}mm"
def test_invalid_gender_returns_422(self):
"""Test that invalid input returns 422, not 500."""
response = requests.post(
"https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
json={
"input_data": {
"input_unit_system": "metric",
"subject": {"gender": "unknown", "input_origin_region": "GLOBAL"},
"anchors": {"body_height": 1700, "body_mass": 70}
},
"output_settings": {
"calculation": {"target_region": "GLOBAL"},
"requested_dimensions": {"bundle": "TORSO"}
}
},
headers={
"X-RapidAPI-Key": LIVE_API_KEY,
"X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
}
)
assert response.status_code == 422
CI/CD configuration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements-test.txt
- run: pytest tests/ -m "not integration" -v
# Runs instantly, no API calls, no secrets needed
integration-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only on main branch
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements-test.txt
- run: pytest tests/integration/ -m "integration" -v
env:
PREDICTION_API_KEY: ${{ secrets.PREDICTION_API_KEY }}
# Runs real API calls, only on main — controlled cost
The key insight is that most of your tests should not hit the live API. Record fixtures once, use them for all unit tests, and reserve live API calls for smoke tests on main branch. This gives you fast, cheap, reliable test coverage without sacrificing the ability to verify the actual API integration periodically.