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).