Skip to content

Recipes

Ready-made patterns for the tests you'll actually write. Each assumes the simcord_env fixture and a guild/channel/member set up at the top — adapt the command and assertion names to your bot.

A prefix command replies

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

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

A slash command, allowed and denied

import discord

async def test_ban_permission_gate(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])
    rando = guild.add_member(simcord_env.create_user("rando"))
    target = guild.add_member(simcord_env.create_user("spammer"))

    denied = await rando.slash(channel, "ban", user=target)
    assert denied.response.content == "You can't do that."
    assert guild.get_ban(target) is None

    allowed = await mod.slash(channel, "ban", user=target, reason="spam")
    assert allowed.ephemeral
    assert guild.get_ban(target) is not None

A confirm/cancel button flow

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

    started = await alice.slash(channel, "delete-data")
    prompt = started.response.message

    confirmed = await alice.click(prompt, label="Confirm")
    assert confirmed.response.content == "Deleted."

Only the invoker can click an ephemeral component

import pytest
import simcord

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

    started = await alice.slash(channel, "private-menu")   # responds ephemerally
    prompt = started.response.message

    with pytest.raises(simcord.SetupError):
        await bob.click(prompt, label="Confirm")           # not bob's message

A modal collects input

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

    shown = await alice.slash(channel, "feedback")
    submitted = await alice.submit_modal(shown, {"name": "Alice", "comment": "Nice!"})

    assert submitted.response.content == "Thanks Alice"

Autocomplete offers the right choices

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

    choices = await alice.autocomplete(channel, "tag", "name", "py")

    assert [c["value"] for c in choices] == ["python", "pytest"]

A context-menu command acts on its target

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

    spam = await bob.send(channel, "buy followers here")

    result = await alice.context_menu(channel, "Report message", spam)   # right-click → Apps
    assert result.ephemeral
    assert "reported" in result.response.content.lower()

A slash command defers, then follows up

async def test_slow_command(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, "report")   # defers ("thinking…"), then follows up

    assert result.deferred
    assert result.followups[-1].content == "Done!"

Assert on an embed with the helper

import simcord

async def test_profile_embed(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, "profile")

    # assert_responded prints what the bot actually did on failure.
    simcord.assert_responded(result, embed_title="alice's profile")

A cooldown blocks, then resets

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

    await alice.send(channel, "!daily")
    await alice.send(channel, "!daily")
    assert "wait" in channel.last_message.content.lower()

    await simcord_env.advance_time(60 * 60 * 24)   # a day, instantly
    await alice.send(channel, "!daily")
    assert "claimed" in channel.last_message.content.lower()

A view times out

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

    await alice.slash(channel, "offer")            # posts a View(timeout=60)
    await simcord_env.advance_time(60)
    assert "expired" in channel.last_message.content

An entity select picks members

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

    result = await alice.slash(channel, "assign")   # posts a UserSelect
    picked = await alice.select(result.response.message, [alice, bob], custom_id="who")
    assert picked.response.content == "Picked alice, bob"

A persistent view survives a restart

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

    result = await alice.slash(channel, "panel")    # posts a persistent View
    panel = result.response.message

    await simcord_env.restart_bot(create_bot())     # a fresh bot instance

    clicked = await alice.click(panel, custom_id="persistent:ping")
    assert clicked.response.content == "pong"

The bot survives an API outage

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

    simcord_env.inject_error("POST", "/channels/*/messages", status=500, times=1)
    await alice.send(channel, "!report")

    # The bot caught the failure and didn't crash; assert your fallback behaviour.
    assert simcord_env.errors == [] or "retry" in channel.last_message.content.lower()

A welcome listener fires on join

async def test_welcome_message(simcord_env):
    guild = simcord_env.create_guild()
    welcome = guild.create_text_channel("welcome")

    newbie = guild.add_member(simcord_env.create_user("newbie"))   # fires on_member_join

    assert newbie.mention in welcome.last_message.content

Assert the bot sent exactly one message

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

    await alice.send(channel, "!announce")

    posts = [c for c in simcord_env.http_log if c[0] == "POST" and "/messages" in c[1]]
    assert len(posts) == 1

Reaction roles

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

    await alice.send(channel, "!rolepanel")        # bot posts the panel
    panel = channel.last_message

    await alice.react(panel, "🎮")
    assert any(r.name == "Gamer" for r in alice.member.roles)

See also