Testing bots¶
darkob's own suite runs entirely offline — no sockets, no tokens. You can do the same for your bot.
Install the test extras¶
Test a handler without a bot¶
Handlers are plain functions. Call them with a stand-in event:
from types import SimpleNamespace
async def test_echo_replies(monkeypatch):
replies = []
fake_event = SimpleNamespace(
text="hello",
chat_id="g0test",
reply=lambda text: replies.append(text) or _noop(),
)
await echo(bot=object(), event=fake_event)
assert replies == ["hello"]
For async def reply(...) handlers, make the stub awaitable:
from types import SimpleNamespace
class FakeEvent:
def __init__(self, text, chat_id="g0test"):
self.text = text
self.chat_id = chat_id
self.replies = []
async def reply(self, text):
self.replies.append(text)
Test routers without a token¶
from darkob import Router
def test_routes_are_registered():
router = Router()
@router.command("start")
async def start(bot, event): ...
assert len(router) == 1
assert router.routes[0].kind == "on_command"
Test conversations¶
MemoryStorage makes FSM tests trivial and isolated:
from darkob import FSMContext, MemoryStorage, StateFilter
async def test_address_flow():
storage = MemoryStorage()
ctx = FSMContext(storage, key="chat")
await ctx.set_state("address")
assert await StateFilter(storage, "address").evaluate(
SimpleNamespace(chat_id="chat")
)
await ctx.update_data(address="Main St")
assert await ctx.get_data() == {"address": "Main St"}
Assert that nothing hits the network¶
Guard the socket layer in a test:
import socket
def test_import_is_offline(monkeypatch):
def boom(*args, **kwargs):
raise AssertionError("network access")
monkeypatch.setattr(socket.socket, "connect", boom)
import darkob # already imported in practice; use a subprocess for a real check
darkob itself checks this in a subprocess, which is the only reliable way to catch import-time I/O.
Testing filters¶
Filters are async predicates over an event-shaped object:
from types import SimpleNamespace
from darkob.bot.filters import Text, IsGroup
async def test_text_filter():
event = SimpleNamespace(original_data={"message": {"text": "hi there"}})
assert await Text("hi").evaluate(event) is True
Golden rules¶
- Never let a test hit a real API.
- Freeze time and randomness when asserting on retries.
- Test the failure path as often as the happy path — that is where darkob's own bugs were found.
darkob.testing¶
Ship-ready fakes, so a handler test needs no token, no network and no hand-written stubs:
from darkob.testing import FakeBot, make_message
async def test_echo(bot=None):
bot = FakeBot()
event = make_message(text="hi")
await echo(bot, event)
assert event.replies == ["hi"]
FakeBot records every call and returns an {"status": "OK"} payload:
await bot.send_message("chat", "hi")
bot.called("send_message") # True
bot.calls_to("send_message") # [(("chat", "hi"), {})]
bot.last_call()[0] # "send_message"
bot.reset() # forget everything
FakeEvent records what a handler does to it:
event = make_message(text="hello", chat_id="g0abc")
await event.reply("thanks")
event.replies # ["thanks"]
event.edits # []
event.deleted # False
Both accept extra keyword attributes, so you can shape the event to match your filters: