SimCord — discord.py testing framework. Simulate Discord, test your bot offline: no network, no token, no ToS.
Project description
SimCord
The discord.py testing framework — simulate Discord, test your bot offline.
Test your discord.py bot with a full virtual Discord environment: no network, no token, no test server, no Terms of Service concerns. SimCord is the missing testing library for discord.py bots.
SimCord is a discord.py testing framework that 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:
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!"
⚠️ Alpha. The core surface (messages, prefix commands, slash commands, components, modals, permissions, reactions, threads, DMs, time control) works; see the parity matrix for the long tail. Unimplemented routes always fail loudly — SimCord never silently fakes success.
Why SimCord?
Unit tests cover your business logic, but the bugs that bite Discord bots live in the
glue: converters, checks, permissions, forgotten tree.sync() calls, double-acknowledged
interactions, oversized embeds. Until now the only way to test that layer was manually,
in a real server. SimCord runs all of discord.py's real machinery — its parsers, cache,
command frameworks and views — against a faithful mock of Discord's REST API and gateway,
entirely in-process.
| 🎯 Real discord.py semantics | Server-side permission checks with authentic error codes (50013 Missing Permissions…), interaction lifecycle rules (40060 on double-ack), role hierarchy, timeouts, ephemeral visibility, validation limits. |
| 🐛 Real bugs caught | Invoking a never-synced slash command fails your test, just like production. Clicking a disabled button is impossible, just like the client. Unhandled bot errors fail the test by default. |
| ⚡ Fast & deterministic | No sleeps, no network, reproducible IDs and timestamps. The framework tracks the bot's tasks and settles after every action. |
| ⏩ Time control | env.advance_time(180) fires view timeouts and resets cooldowns instantly — no real waiting. |
| 🔍 Debuggable failures | Failing tests automatically include a transcript of every gateway event and REST call — exactly what your bot did, in order. |
| 📢 Loud gaps | Anything not implemented raises RouteNotImplemented naming the route. Never silent fake success. |
Install
pip install simcord[pytest]
Requires Python 3.11+ and discord.py 2.7+. Zero 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:
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
Buttons, selects, modals, context menus, autocomplete, reactions, threads, DMs, fault
injection and more: see the documentation.
Prefer explicit control? async with simcord.run(bot) as env: works in any async
test framework.
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 code running unmodified.
test ──► builders/actors ──► virtual backend (single source of truth)
│ │
gateway payloads ▼ ▼ REST responses
ConnectionState.parsers FakeHTTPClient route table
│ ▲
▼ │
your real, unmodified bot
Details in the architecture docs.
discord.py testing — common use cases
SimCord covers the full range of discord.py bot testing scenarios:
- discord.py unit testing — test individual commands in isolation
- discord.py integration testing — test full command flows with permissions, roles and channels
- discord.py mock events — fire any gateway event (member join, reaction add, voice state…) without a real server
- discord.py mock Discord — a full in-memory Discord server your bot can't tell from the real thing
- discord.py command testing — prefix commands, slash commands, context menus, autocomplete
- discord.py interaction testing — buttons, selects, modals, ephemeral responses, deferred replies
- discord.py bot testing without a token — no
.env, no test guild, no rate limits
Comparison
| 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 | ✅ | ❌ | — |
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file simcord-0.8.1.tar.gz.
File metadata
- Download URL: simcord-0.8.1.tar.gz
- Upload date:
- Size: 300.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff658300d1dc71c80cf22aab427e441d82d2a7ef3f62339e7705998ec6b7055d
|
|
| MD5 |
0d111de060c3970d22051c2f7592a995
|
|
| BLAKE2b-256 |
ed14179f9312803688c2cb9ca859e63dee995ec591cb06e740d2855bb7ce8100
|
Provenance
The following attestation bundles were made for simcord-0.8.1.tar.gz:
Publisher:
release.yml on SilentHacks/simcord
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
simcord-0.8.1.tar.gz -
Subject digest:
ff658300d1dc71c80cf22aab427e441d82d2a7ef3f62339e7705998ec6b7055d - Sigstore transparency entry: 1816984910
- Sigstore integration time:
-
Permalink:
SilentHacks/simcord@f8a35a392e91f0c6c34baea5976bdded24080d5f -
Branch / Tag:
refs/tags/v0.8.1 - Owner: https://github.com/SilentHacks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f8a35a392e91f0c6c34baea5976bdded24080d5f -
Trigger Event:
push
-
Statement type:
File details
Details for the file simcord-0.8.1-py3-none-any.whl.
File metadata
- Download URL: simcord-0.8.1-py3-none-any.whl
- Upload date:
- Size: 108.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3de51c6e956abcd53c308d80e55097d305030041579d9c22859529e689e9c31
|
|
| MD5 |
fca97ac317c8304ad0f90e9b0df0e11c
|
|
| BLAKE2b-256 |
dcd5b1698246b666a10f253abf664e08883f7c331caab02c95e9d7d4adb4e5ba
|
Provenance
The following attestation bundles were made for simcord-0.8.1-py3-none-any.whl:
Publisher:
release.yml on SilentHacks/simcord
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
simcord-0.8.1-py3-none-any.whl -
Subject digest:
e3de51c6e956abcd53c308d80e55097d305030041579d9c22859529e689e9c31 - Sigstore transparency entry: 1816985701
- Sigstore integration time:
-
Permalink:
SilentHacks/simcord@f8a35a392e91f0c6c34baea5976bdded24080d5f -
Branch / Tag:
refs/tags/v0.8.1 - Owner: https://github.com/SilentHacks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f8a35a392e91f0c6c34baea5976bdded24080d5f -
Trigger Event:
push
-
Statement type: