Back to writing

How to Test a discord.py Bot Without Connecting to Discord

SimCord runs your real bot against an in-memory Discord backend, making commands, interactions, permissions and events testable with pytest.

4 min read

GitHub Repo: https://github.com/SilentHacks/simcord

In the agentic development era, systems need to have an interface for agents to use. Whether it be MCP, CLI or computer use, there needs to be something. Giving the agent a way to check its work and validate its output is extremely important for any task loop.

When it comes to Discord bots, we have the obstacles that are Terms of Services. We can’t use any kind of automation to interact with Discord, which kills any idea of end to end testing. Since now, the only (legit) way to test your Discord bot has been to spin up the bot and manually put in commands, checking the output over Discord and whatever gets thrown in the terminal.

Yes, you can decouple the application logic with Discord interaction, and unit test on just that. But then you can’t test any Discord primitives at all really. To combat this, I set out on making a library that can simulate a backend for discord.py (the unofficial de-facto Python API for Discord).

I had a few goals for what this should be able to achieve:

  1. Ability to plug straight into existing projects with minimal/no change
  2. Support most/all Discord interactions
  3. A clean and intuitive API for creating tests

How it works

To do this, SimCord intercepts the two places that discord.py normally communicates with Discord:

  1. Outgoing HTTP requests and
  2. Incoming Gateway events

Think of SimCord as a tiny, fake Discord backend running all inside the test process.

SimCord architecture between a discord.py bot and its in-memory Discord backend

This means from your bot’s perspective, nothing is different. It’s talking the language Discord knows, and it’s getting back replies it understands.

How it was made

To achieve this, SimCord needs to reproduce as much of Discord’s behaviour as is (reasonably) possible. It needs to understand all the HTTP routes a bot can call and produce all the Gateway events Discord would send in response.

Manually maintaining a list of endpoints would quickly get outdated, so we turned to discord.py itself as the source of truth. For HTTP, SimCord inspects every public method on discord.py’s HTTPClient and extracts the Route(...) declarations used by those methods. These contain the HTTP method and path, such as:

POST /channels/{channel_id}/messages

This gives us a concrete list of routes to implement. SimCord registers a handler function with a decorator for each route. When discord.py makes that request, SimCord’s router matches the path, extracts values like channel ID, and calls the corresponding handler.

The handler then has to reproduce the part of Discord responsible for that endpoint. It validates the request, checks permissions, updates the in-memory backend, serialises a Discord-shaped response, and emits any Gateway events that Discord would normally produce.

To figure out exactly what events Discord emits, we used Discord’s Gateway documentation and discord.py’s own event parser. Each feature was built one by one, ensuring it kept parity with Discord’s own mechanisms. For example, a feature like reactions would include:

  1. The relevant HTTP routes
  2. The in-memory state changes
  3. The corresponding Gateway events
  4. The Discord-shaped payload serialisers
  5. Integration tests passing those payloads through discord.py’s actual parsers

Using discord.py’s actual parsers was especially useful. If SimCord produced an invalid or incomplete payload, discord.py would fail to parse it, or populate its cache incorrectly.

Through this process, each Discord feature was implemented slice by slice. First came messages, followed by reactions, threads, interactions, and the rest of the commonly used Discord surface. (This was a job that an AI agent could mechanically churn through very competently).

Conclusion

Not every route is handled, but I would claim the majority of what is actually useful to discord.py developers has been covered. Previous attempts at a Discord testing framework have not been maintained or exhaustive over Discord’s surface, so SimCord provides real and novel value to developers making Discord bots.

And especially in the current age of AI agents, this represents a way to close the testing loop and let the agents validate their own output. I hope people (and their agents) find it useful.

Get started with SimCord here: https://simcord.readthedocs.io/en/latest/