Docs / Developers / Production / Testing
ShipSDK 0.10.1

Testing

These docs are public. To publish a plugin you need a YourBot account: sign in to open the Dev Portal.

Plugins test like any other Python code — no Docker, no platform connection, no Discord mocks. Import from yourbot_sdk.testing:

from yourbot_sdk.testing import MockContext, make_event

def test_ping_replies_pong():
    ctx = MockContext()
    event = make_event("message_create", content="!ping", channel_id="42")
    on_message(ctx, event)            # the handler from your __main__.py

    assert len(ctx.messages_sent) == 1
    sent = ctx.messages_sent[0]
    assert sent["channel_id"] == "42"
    assert sent["content"] == "Pong!"

def test_kv_counter_increments():
    ctx = MockContext()
    ctx.kv.increment("hits")
    ctx.kv.increment("hits")
    assert ctx.kv.get("hits") == 2

def test_capability_gate():
    ctx = MockContext(capabilities=["discord:send_message"])
    assert ctx.has_capability("discord:send_message")
    assert not ctx.has_capability("storage:sql")

MockContext enforces capabilities by default. Calling a gated method without the matching capability raises CapabilityError, exactly like production — so a passing test means a working manifest. With no capabilities= argument you get the full standard set except proxy:websocket and events:message_content; tests that need those must pass them explicitly, e.g. MockContext(capabilities=[..., "proxy:websocket"]). capabilities=[] means none. Pass MockContext(strict_capabilities=False) to opt out. v0.6.1

Signature-strict since v0.8.5: the mocks enforce production's keyword-only arguments. ctx.interaction.respond(…), ctx.http.post(…), ctx.sql.query(…) and friends raise TypeError under MockContext exactly as they would in production, so a green test suite can't hide a signature bug. On SDK 0.8.4 and earlier the mocks were looser and accepted these positionally; use the keyword forms shown in these docs either way.

What MockContext records

Every Discord side-effect is captured for assertion. Read these as lists of dicts in the order they were called:

ctx.messages_sent             ctx.messages_edited            ctx.messages_deleted
ctx.roles_added               ctx.roles_removed
ctx.members_banned            ctx.members_kicked             ctx.modals_sent
ctx.kv_writes                 ctx.log_lines                  ctx.interaction.responses
ctx.metrics.recorded          ctx.sql.executed
ctx.discord.messages_pinned   ctx.discord.messages_unpinned
ctx.discord.reactions_added   ctx.discord.members_timed_out  ctx.ws.ensured

Everything else your handlers touch is recorded on ctx.discord (webhooks, channels, threads, nicknames, permissions) and ctx.ws (ensured, sent, closed, allowed_hosts).

Deterministic time and canned data: MockContext(clock=MockClock(start=1000.0)) lets you clock.advance(31) for cooldown tests (a bare MockClock() is wall-clock passthrough and cannot be advanced), and ctx.discord.set_messages([…]) feeds get_messages / iter_messages.

Mock outbound HTTP

def test_calls_external_api():
    ctx = MockContext()
    ctx.http.mock_response(
        "api.example.com/status",
        status=200,
        body='{"online": true}',
    )
    my_status_handler(ctx, make_event("message_create", content="!status"))
    assert ctx.http.requests[0]["url"].startswith("https://api.example.com")

make_event ships sensible defaults for the 16 core event types — pass overrides as kwargs (unknown types return just your overrides).

YourBot docs Reference tables are generated from the code that is running. Ask in Discord Suggest a correction