Skip to main content

SimCord — discord.py testing framework. Simulate Discord, test your bot offline: no network, no token, no ToS.

Project description

SimCord

Test your discord.py bot against a simulated Discord — no network, no token, no test server.

CI Docs PyPI Python discord.py License: MIT

Quickstart · Mental model · Documentation · Parity matrix · Contributing


SimCord gives your bot a fake but faithful Discord to run against. Simulate users sending messages, invoking slash commands, clicking buttons and submitting modals — then assert on exactly what your bot did. It all runs in-process, with no network and no token.

async def test_ping(simcord_env):
    channel = simcord_env.create_guild().create_text_channel("general")
    alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))

    await alice.send(channel, "!ping")          # full gateway round trip

    assert channel.last_message.content == "Pong!"

Why SimCord?

Your unit tests cover your business logic. The bugs that actually break Discord bots live in the glue: converters, checks, permissions, a forgotten tree.sync(), a double-acknowledged interaction, an oversized embed. That layer has historically only been testable by hand, in a real server.

SimCord runs discord.py's real machinery — its parsers, cache, command frameworks and views — against an in-memory model of Discord's REST API and gateway. Your bot code runs unmodified and can't tell the difference.

🎯 Authentic semantics Server-side permission checks with real error codes (50013 Missing Permissions), interaction lifecycle rules (40060 on double-ack), role hierarchy, timeouts, ephemeral visibility and validation limits.
🐛 Catches real bugs Invoking a never-synced slash command fails your test, just like production. Clicking a disabled button is impossible, just like the client. An unhandled error in your bot fails the test by default.
Fast & deterministic No sleeps, no network, reproducible IDs and timestamps. SimCord tracks the bot's tasks and settles the event loop after every action, so assertions never flake.
Time control env.advance_time(180) fires view timeouts and resets cooldowns instantly — no real waiting.
🔍 Readable failures A failing test prints a transcript of every gateway event and REST call, in order: exactly what your bot did.
📢 No silent fakes Anything unimplemented raises RouteNotImplemented naming the route. Gaps fail loudly rather than returning a wrong answer.

Install

pip install simcord[pytest]

Requires Python 3.11+ and discord.py 2.7+. No dependencies beyond discord.py itself.

Quickstart

Tell the bundled pytest plugin how to build your bot:

# conftest.py
import pytest
from mybot import create_bot   # however your project builds its commands.Bot

@pytest.fixture
def simcord_bot():
    return create_bot()

Then write tests against the simcord_env fixture — it hands you a running environment with the bot already logged in and at READY:

import discord

async def test_ban_slash_command(simcord_env):
    guild = simcord_env.create_guild()
    channel = guild.create_text_channel("mod")
    mods = guild.create_role("Mods", permissions=discord.Permissions(ban_members=True))
    mod = guild.add_member(simcord_env.create_user("mod"), roles=[mods])
    target = guild.add_member(simcord_env.create_user("spammer"))

    result = await mod.slash(channel, "ban", user=target, reason="spam")

    assert result.ephemeral
    assert result.response.content == f"Banned {target.mention}: spam"
    assert guild.get_ban(target) is not None

async def test_offer_expires(simcord_env):
    channel = simcord_env.create_guild().create_text_channel("general")
    alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))

    result = await alice.slash(channel, "offer")    # bot replies with a View(timeout=180)
    await simcord_env.advance_time(180)             # instant — the view times out

    assert "expired" in channel.last_message.content

Not using pytest? async with simcord.run(bot) as env: gives you the same env in any async test framework.

The mental model

Every SimCord test is three moves: arrange the world, act as a user, assert the result. Three kinds of object map to those moves.

Role Nature
Builders Arrange the scenario — guilds, channels, roles, members. Synchronous and omnipotent: the test is the narrator, so no permission checks.
Actors Act as a real human — send, click, run a command. Async and permission-checked: an actor can only do what that user physically could in the client.
Queries Assert what happened. Return real discord.py objects from the bot's own cache, so you assert with plain assert — no DSL.
import discord

async def test_welcome_on_join(simcord_env):
    guild   = simcord_env.create_guild()                       # builder
    welcome = guild.create_text_channel("welcome")             # builder
    newbie  = guild.add_member(simcord_env.create_user("ann")) # builder — fires the join event

    assert f"Welcome {newbie.mention}" in welcome.last_message.content   # query

Two details that make tests robust:

  • Actors wait for the bot to finish reacting. Each verb settles the loop — running callbacks, draining asyncio.sleep chains — before returning, so the reply is already there when the next line runs. No sleeps, no flakes. If a handler hangs, settling fails fast with the pending tasks listed.
  • Impossible setups raise SetupError, not a bot failure. Speaking in a channel a user can't see, or clicking a disabled button, points at your test, distinct from a bug in the bot.

See Core concepts for the full picture.

What you can test

Area Actor verbs Covers
Messages & prefix commands send · edit · delete · typing Content, embeds, attachments, mentions, the commands.Bot prefix framework.
Slash commands slash · autocomplete App command tree, tree.sync(), options, converters, checks, autocomplete.
Context menus context_menu User and message commands.
Components & modals click · select · submit_modal Buttons, selects, modals, View timeouts, persistent views across restarts.
Reactions react · unreact Reaction add/remove events and wait_for.
Polls vote · remove_vote Poll answers and results.
Voice & events join_voice · leave_voice · set_voice · subscribe_event Voice state, scheduled-event subscriptions.
DMs send_dm Direct-message channels and flows.

Responses come back as a rich InteractionResult exposing acknowledged, deferred, ephemeral, response, followups and modal. Threads, permissions, role hierarchy, intents and audit logs are modelled too — the parity matrix records exactly what's implemented.

Configuration & diagnostics

Pass options to simcord.run(bot, ...), or per-test via the @pytest.mark.simcord(...) marker on the simcord_env fixture:

Option Default Effect
strict_sync True Invoking an unsynced slash command fails the test, as in production.
check_errors True Errors your bot swallowed are re-raised at test teardown unless inspected, so bugs can't pass silently.
approved_intents all Simulate the developer-portal privileged-intent toggles; a missing intent raises PrivilegedIntentsRequired on connect.
@pytest.mark.simcord(strict_sync=False)
async def test_unsynced_command(simcord_env):
    ...

When something goes wrong, the env tells you what happened:

  • env.transcript() — the ordered log of gateway events and REST calls (auto-attached to failing pytest tests).
  • env.http_log — every REST request the bot made, to assert on or inspect.
  • env.errors — exceptions the bot swallowed.
  • env.inject_error("POST", "/channels/*/messages", status=500) — make matching REST calls fail, to test your bot's error handling.
  • env.restart_bot() — restart the bot while the virtual world persists, to prove persistent views re-attach.

How it works

discord.py has two narrow seams: every REST call funnels through HTTPClient.request, and every gateway event enters through ConnectionState.parsers. SimCord replaces the first with a fake routed to an in-memory backend — a single source of truth for guilds, channels, members, messages, commands and interactions — and injects Discord-shaped payloads through the second. Everything between those seams, which is everything your bot touches, is real discord.py running unmodified.

test ──► builders/actors ──► virtual backend (single source of truth)
                                   │                     │
                  gateway payloads ▼                     ▼ REST responses
                  ConnectionState.parsers        FakeHTTPClient route table
                                   │                     ▲
                                   ▼                     │
                                  your real, unmodified bot

More in the architecture docs.

SimCord vs. the alternatives

SimCord dpytest Manual test server
No network / no token
Real discord.py internals Partial
Slash commands & components
Authentic error codes
Time control
Failure transcripts
Maintained for discord.py 2.x

Coming from dpytest? See the migration guide.

Documentation

🚀 Quickstart Get a first test running.
🧠 Core concepts Builders, actors, queries — the model.
📖 Guides Messages, interactions, components, permissions, threads, time control, diagnostics.
🍳 Recipes Copy-paste patterns for common cases.
📋 Parity matrix Exactly what's implemented.
🔖 API reference Every public object and verb.

Contributing

See CONTRIBUTING.md. Bug reports with a failing test are gold. If your bot hits an unimplemented route, the error names it — please open a parity gap issue.

License

MIT. Unofficial — not affiliated with Discord Inc. or the discord.py project.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simcord-1.1.0.tar.gz (359.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simcord-1.1.0-py3-none-any.whl (125.6 kB view details)

Uploaded Python 3

File details

Details for the file simcord-1.1.0.tar.gz.

File metadata

  • Download URL: simcord-1.1.0.tar.gz
  • Upload date:
  • Size: 359.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for simcord-1.1.0.tar.gz
Algorithm Hash digest
SHA256 f20dada11616ad46d30a58d2d5000d0efb62ea20614c1486d23edb2c5b20cac7
MD5 4c837c99c847fe39c9a258c3e19632bd
BLAKE2b-256 cbdec04f2ba8b1fd6ad958ec917c53d6ac7c93605f5cc03d5f966297181d9bcf

See more details on using hashes here.

Provenance

The following attestation bundles were made for simcord-1.1.0.tar.gz:

Publisher: release.yml on SilentHacks/simcord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file simcord-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: simcord-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 125.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for simcord-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9d9e0602496b550346f0e5d37fc296a3f103ca8a2141959847ba40745ed8df96
MD5 4f4323cef023c17c2cea57e2c41fc13b
BLAKE2b-256 625a84da91dd41c0ff0de2868e3e6515d5fe6b26c64704fd0249fc8fac0a568e

See more details on using hashes here.

Provenance

The following attestation bundles were made for simcord-1.1.0-py3-none-any.whl:

Publisher: release.yml on SilentHacks/simcord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page