Back to writing

Testing Discord Integrations Offline with simcord

A practical testing model for Python Discord integrations, grounded in simcord’s public description as an offline discord.py testing framework.

5 min read

A Discord integration is an awkward boundary to test. The code may be ordinary Python, but its inputs usually arrive through an event framework, and its effects are often messages, reactions, or state changes in a remote service. The public description of simcord is deliberately focused: it is a Python framework for testing discord.py integrations offline.

That description is enough to establish a useful engineering goal without inventing an API. An offline test should be able to exercise application code without opening a Discord connection, depending on a live guild, or waiting for a real event. The exact helpers and adapters belong to the framework itself. The testing model below is the important part.

Start with a small boundary

The first design choice is to separate a command’s decision from the transport that carries it. A command can accept a context-like object and ask it to send a response. The application owns the decision. The test owns the fake context and its recorded effects.

from dataclasses import dataclass, field


@dataclass
class FakeContext:
    author_id: int
    sent: list[str] = field(default_factory=list)

    async def send(self, message: str) -> None:
        self.sent.append(message)


async def greet(ctx: FakeContext) -> None:
    await ctx.send(f"hello user {ctx.author_id}")

This snippet is a framework-neutral sketch, not a claim about simcord’s concrete API. It shows the observable contract a test needs: given an input context, the command records one response. A real discord.py command can be adapted to the same shape while simcord supplies the offline event and object model.

Test effects, not implementation details

An integration test should assert what a user or another component can observe. For a command, that might mean the content of a message and the number of messages sent. It should not require a particular helper method to have been called internally.

import pytest


@pytest.mark.asyncio
async def test_greet_sends_one_message() -> None:
    context = FakeContext(author_id=42)

    await greet(context)

    assert context.sent == ["hello user 42"]

The test has no network setup and no dependency on a Discord account. It is fast because the test replaces the boundary, not because it skips the command. That distinction matters when a command grows more branches. A test can still cover permission checks, missing configuration, and a successful response while remaining local and deterministic.

Model events as input data

Discord integrations are event-driven. A useful offline test therefore makes event data explicit. Instead of relying on a global client or a hidden fixture, represent the small set of fields a handler reads.

from dataclasses import dataclass


@dataclass(frozen=True)
class Message:
    author_id: int
    content: str


async def moderate(message: Message, ctx: FakeContext) -> None:
    if message.content.startswith("!status"):
        await ctx.send("ready")

A test can now describe the event that matters and assert the resulting effect. More complicated handlers may need channel, guild, role, or interaction data. Add those fields only when application behavior reads them. Keeping fixtures narrow makes failures easier to interpret and reduces the chance that a test passes because of accidental fixture state.

The value of a framework such as simcord is in making this style practical for discord.py code. Its public purpose does not imply that every Discord feature is reproduced, and a responsible test suite should not assume that it is. Test the application logic that the offline model can represent. Keep a smaller set of explicitly documented checks for behavior that depends on Discord itself.

Keep remote checks separate

Offline tests and remote checks answer different questions:

  1. Does the command make the right decision for a given event?
  2. Does the integration use the expected Discord objects and permissions?
  3. Does the deployed bot behave correctly in a real guild?

The first question is a good target for fast local tests. The second may need a small integration layer around the framework. The third needs a controlled environment and should not be disguised as an offline unit test. Naming these layers avoids a common failure mode where a test suite appears comprehensive but never exercises the real boundary.

The discord.py documentation is the authority for the library’s runtime behavior. The simcord repository is the authority for the offline framework’s supported surface. Read both when deciding whether a fixture represents a supported behavior or merely a convenient local fake.

Make failures diagnostic

A useful offline test should leave a short path from failure to cause. Prefer assertions that show the event, the expected effect, and the actual effect. When several commands share a fixture, reset recorded effects between cases so one test cannot hide another’s output.

async def run_case(content: str) -> list[str]:
    context = FakeContext(author_id=7)
    await moderate(Message(author_id=7, content=content), context)
    return context.sent


@pytest.mark.asyncio
async def test_status_event_is_observable() -> None:
    assert await run_case("!status") == ["ready"]


@pytest.mark.asyncio
async def test_other_event_is_ignored() -> None:
    assert await run_case("hello") == []

The two cases document a transition: one event produces a response, and an unrelated event does not. That invariant is more valuable than a test that only checks the handler was entered. As the integration changes, these tests explain what must remain stable.

A modest promise

Offline testing is not a replacement for Discord. It is a way to make the application layer cheap to exercise and to reserve remote checks for questions that genuinely require a remote service. simcord’s public description points directly at that boundary for Python and discord.py projects.

The honest claim is limited but useful: a framework for offline Discord testing can help make event handling deterministic, repeatable, and easier to review. The test suite still needs clear contracts, narrow fixtures, and a separate plan for behavior that only Discord can validate.