Skip to content

Testing and Mocking

This page shows how to test code built on erlc-api.py without calling the live PRC API.

Fake Async Client

Use small fake clients for application tests:

class FakeAPI:
    async def players(self, **_kwargs):
        return []

    async def command(self, command, **_kwargs):
        return {"command": command, "success": True}

Pass the fake into your service, command handler, or watcher wrapper.

Fake Sync Client

class FakeSyncAPI:
    def players(self, **_kwargs):
        return []

This keeps CLI and script tests fast and deterministic.

Fixture-based Raw Payloads

Use raw=True output or official examples as fixtures:

payload = [{"Player": "Avi:123", "Team": "Police"}]

Then decode through public model functions or client-facing code paths. Prefer public APIs where possible; raw fixture tests should catch payload drift without locking every application test to PRC JSON.

Command Dry-runs

Use dry_run=True to test command composition without sending HTTP:

from erlc_api import cmd

result = await api.command(cmd.pm("Player", "hello"), dry_run=True)
assert result.raw["command"] == ":pm Player hello"

Webhook Payload Tests

Test decoding separately from signature verification:

from erlc_api.webhooks import decode_event_webhook_payload

event = decode_event_webhook_payload({"Message": ";ping"})
assert event.command.command_name == "ping"

For signature verification tests, keep raw bytes and headers together. Do not verify a reserialized body.

Waiter And Watcher Tests

Use fake clients that return different snapshots over time:

class ChangingAPI:
    def __init__(self):
        self.calls = 0

    async def players(self, **_kwargs):
        self.calls += 1
        return [] if self.calls == 1 else [{"Player": "Avi:123"}]

Keep polling intervals tiny in tests and timeouts short.

Optional Dependency Tests

For extras, assert helpful errors when dependencies are missing. Do not import optional libraries at module import time in utility modules.

Rate Limiter Tests

Use injected clocks and sleep functions to avoid real delays:

from erlc_api.ratelimit import RateLimiter

now = 100.0
sleeps = []

limiter = RateLimiter(now=lambda: now, sleep=sleeps.append)
limiter.after_response(
    "GET",
    "/v2/server",
    {"X-RateLimit-Bucket": "global", "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": "105"},
)

assert limiter.before_request("GET", "/v2/server") == 5.0

For client integration tests, pass fake transports. Rate limiting is enabled by default, so pass rate_limited=False only when a test specifically needs the old no-limiter behavior.

Error Code Tests

from erlc_api.error_codes import explain_error_code

info = explain_error_code(4001)
assert info.retryable is True

Also assert that top-level import erlc_api does not import erlc_api.error_codes or erlc_api.ratelimit.

Workflow Utility Tests

The 2.3 and 2.4 workflow utilities are easiest to test with fake clients and already decoded models:

from erlc_api.bundle import AsyncBundle
from erlc_api.status import StatusBuilder
from erlc_api.cache import AsyncCachedClient
from erlc_api.vehicles import VehicleTools
from erlc_api.emergency import EmergencyCallTools

bundle = await AsyncBundle(fake_api).dashboard()
status = StatusBuilder(bundle).build()
cached = AsyncCachedClient(fake_api, ttl_s=5)
vehicles = VehicleTools(bundle.vehicles or [])
calls = EmergencyCallTools(bundle.emergency_calls or [])

Recommended scenarios:

  • location geometry and map URL helpers with fixed coordinates;
  • missing Pillow errors for MapRenderer;
  • bundle preset expansion and invalid include names;
  • rule matches and callbacks without command side effects;
  • multi-server per-server error collection;
  • cache hits, misses, TTL expiry, and command non-caching;
  • command-flow previews and missing template placeholders;
  • Discord payload dictionaries without importing a Discord library.
  • vehicle catalog parsing, plate normalization, duplicate plates, and owner joins through bundle.player_vehicles;
  • emergency-call active/unresponded filters and nearest-call helpers;
  • PermissionLevel, Filter.permission_at_least(...), wanted-star filters, and watcher wanted-change events;
  • CommandResult.command_id parsing from commandId, CommandId, and command_id.

Keep top-level import tests updated whenever a new utility module is added.

Common Mistakes

  • Calling the live PRC API from unit tests.
  • Sleeping for real production intervals in waiter/watcher tests.
  • Testing webhook verification with JSON strings instead of raw bytes.
  • Asserting exact .to_dict() output when the test only needs one field.
  • Letting a rule, cache, or command-flow test call api.command() accidentally.

Previous Page: Errors and Troubleshooting | Next Page: Getting Started