Test a Carrier Lookup Integration Offline with Response Fixtures
Test flat carrier fields, empty results, ordered multi responses and error branches without real contact data or paid API calls.

Inject the transport and test explicit synthetic response fixtures before allowing a CarrierLookup consumer to use the network.
Carrier lookup API mock tests should replace the HTTP transport, deny accidental network access and assert how your application handles each documented outcome. Synthetic fixtures can test consumer logic without submitting phone numbers or spending balance.
Put the test seam before the HTTP request
A CRM importer can pass its happy-path test and still label an undetermined number as invalid. It can also pass a mocked browser test while its backend makes a real paid request. Place your test seam at the backend function that performs the outbound HTTP exchange.
The consumer should accept a transport dependency instead of discovering a real client internally. The injected test transport receives the intended path and request body, asserts them and returns a synthetic envelope. There is no API key in the test. Keep live credentials out of the CI job as well.
Use a separate network restriction as a backstop. A mock that returns convenient data is not proof that every outbound call was intercepted. Deny outbound traffic at the CI container or runner boundary, and fail the job on attempted network access. A language-level guard helps catch common mistakes but cannot cover subprocesses or every networking library.
Define the outcomes before writing fixtures
Our synchronous carrier service uses service_type=carrier. Single responses have flat data fields, including identifier, carrier, underlying_carrier, number_type, country_code, region and city. Reading data.extra.carrier tests an invented shape.
| Fixture | Consumer assertion |
|---|---|
| Code 0 with allocation fields | Keep original-allocation values |
| Code 0 with all allocation fields empty | Completed lookup with unavailable values |
| Code 42200 | Undetermined; do not create an empty success |
| Multi item with exists=false | No normal result; do not mark the number disconnected |
| Code 42901 | Busy branch; no successful CRM update |
| Code 50400 | Timeout branch; no successful CRM update |
Carrier values describe original allocation. They should not populate a CRM field called current network. Similarly, neither an empty allocation result nor exists=false establishes phone validity. Use separate application outcome labels so a later reporting query cannot merge these states accidentally.
Run a small synthetic fixture suite
Save the following as carrier_consumer_test.py and run python3 carrier_consumer_test.py. It uses Python's standard library. The example is a minimal consumer seam, with synthetic identifiers and deliberately chosen values; it never contacts CarrierLookup. Replace the demonstration client and classifier with your own adapter while keeping the fixture assertions.
import socket
import unittest
from unittest.mock import patch
FIELDS = ("carrier", "underlying_carrier", "number_type",
"country_code", "region", "city")
A, B = "+12025550101", "+12025550102" # Synthetic test data only.
def classify(envelope):
code = envelope["code"]
if code != 0:
return {42200: "undetermined", 42901: "busy",
50400: "timeout"}.get(code, "error")
data = envelope["data"]
if data["service_type"] != "carrier":
raise ValueError("unexpected service")
return "completed" if data["carrier"] else "completed_empty"
def client(transport, identifiers):
multi = len(identifiers) > 1
path = "/api/v1/batch-check" if multi else "/api/v1/check"
body = {"service_type": "carrier"}
body["identifiers" if multi else "identifier"] = (
identifiers if multi else identifiers[0])
return transport(path, body)
def completed(identifier, carrier=""):
return {"code": 0, "msg": "synthetic fixture", "data": {
"service_type": "carrier", "identifier": identifier,
**dict.fromkeys(FIELDS, ""), "carrier": carrier}}
class ConsumerTests(unittest.TestCase):
def setUp(self):
self.guard = patch.object(
socket.socket, "connect",
side_effect=AssertionError("network forbidden"))
self.guard.start()
self.addCleanup(self.guard.stop)
def test_single_flat_data_and_outcomes(self):
for value, expected in [("Example Allocation", "completed"),
("", "completed_empty")]:
def fake(path, body):
self.assertEqual(path, "/api/v1/check")
self.assertEqual(body, {"service_type": "carrier",
"identifier": A})
return completed(A, value)
result = client(fake, [A])
self.assertNotIn("extra", result["data"])
self.assertNotIn("registered", result["data"])
self.assertEqual(classify(result), expected)
def test_error_branches(self):
for code, expected in [(42200, "undetermined"),
(42901, "busy"), (50400, "timeout")]:
with self.subTest(code=code):
result = client(lambda path, body: {
"code": code, "msg": "synthetic error",
"data": None}, [A])
self.assertEqual(classify(result), expected)
def test_multi_order_and_no_result(self):
def fake(path, body):
self.assertEqual(path, "/api/v1/batch-check")
self.assertEqual(body, {"service_type": "carrier",
"identifiers": [B, A]})
return {"code": 0, "msg": "synthetic fixture", "data": {
"service_type": "carrier", "total": 2,
"succeeded": 1, "failed": 1, "results": [
{"identifier": B, "exists": True,
**dict.fromkeys(FIELDS, "")},
{"identifier": A, "exists": False}]}}
data = client(fake, [B, A])["data"]
rows = data["results"]
self.assertEqual(data["service_type"], "carrier")
self.assertEqual([r["identifier"] for r in rows], [B, A])
self.assertEqual(len(rows), data["total"])
outcomes = ["completed" if r["exists"] else "no_result"
for r in rows]
self.assertEqual(outcomes, ["completed", "no_result"])
self.assertNotIn("carrier", rows[1])
def test_guard_rejects_an_escape(self):
with socket.socket() as sock:
with self.assertRaisesRegex(AssertionError, "network forbidden"):
sock.connect(("127.0.0.1", 9))
if __name__ == "__main__":
unittest.main()
The example returns already-decoded envelopes from its transport. It tests request construction and outcome handling, rather than HTTP parsing. Keep fixtures for HTTP status, headers and malformed JSON at your real transport boundary too. In particular, a 42901 response includes Retry-After; a response-header fixture should verify that your transport preserves it for the caller. Do not hard-code a new retry interval into the outcome classifier.
The socket guard intentionally tests its own failure path. It blocks this Python process's socket connection method; the separate CI egress restriction remains necessary for other paths. An accidental paid call should fail the job, not become an unnoticed extra fixture.
Make multi-result assertions sensitive to ordering mistakes
The multi fixture submits B before A. That matters: a test that submits alphabetically sorted identifiers and receives the same sorted order can miss an adapter that sorts responses. Match every returned identifier to its input position and assert the total length.
The first multi item produces a completed result with empty allocation values. The second has exists=false and no carrier fields. Preserve that difference when mapping to application records. If your production adapter drops failed rows, the fixture should fail because later rows could otherwise shift onto the wrong contacts.
Add one assertion at the actual CRM mapping function: an undetermined or failed lookup must leave the previous allocation observation intact or mark it explicitly stale under your policy. It must not overwrite the record with “invalid number.” The same test should reject deriving current location from the allocation region or city.
A synchronous multi timeout fails the whole request without partial results. Test the whole-request timeout branch separately from a successful envelope containing an unsuccessful item. Those are different inputs to the consumer.
Keep offline confidence separate from live verification
These tests establish how your code responds to the contract represented by the fixtures. They do not demonstrate current provider availability, country acceptance, account permissions or billing. A fabricated fixture also cannot prove that a live provider will return a particular allocation field for a particular number.
Keep the fixture provenance beside the test: the CarrierLookup API reference, the date reviewed and the fields your adapter consumes. Update fixtures when a documented contract changes, then examine whether old application data needs a compatible read path.
Run a separately authorized live smoke test when integration verification requires it, using approved input and its own balance budget. Do not make ordinary CI depend on that call. Start by replacing one live-client import in your test suite with an injected transport and confirming that an attempted network escape fails immediately.
Learn More
Choose the product information that fits the next step in your workflow.