teremock 0.5.3

Fast integration testing library for teloxide Telegram bots
Documentation

teremock

Telegram · Realistic · Mocking — A fast, ergonomic testing library for teloxide bots.

Crates.io Documentation CI teloxide License


teremock enables you to write fast, reliable integration tests for your Telegram bots without network access, API tokens, or external services. Run your entire test suite in seconds, not minutes.

use teremock::{MockBot, MockMessageText};

#[tokio::test]
async fn test_hello_command() {
    let mut bot = MockBot::new(MockMessageText::new().text("/hello"), handler_tree()).await;

    bot.dispatch().await;

    let responses = bot.get_responses();
    assert_eq!(responses.sent_messages.last().unwrap().text(), Some("Hello, World!"));
}

Why teremock?

Lightning Fast

Tests run 15-30x faster than traditional approaches. The mock server starts once and persists across all dispatches within a test — no server restart overhead between interactions.

50 sequential dispatches: ~2 seconds (teremock)
50 sequential dispatches: ~30-60 seconds (server-per-dispatch)

True Black-Box Testing

Test your bot the way users experience it. Send messages, click buttons, trigger commands — then verify the responses. No internal state manipulation, no implementation coupling.

Multi-Step Conversations in One Test

Test complete user flows without juggling multiple test functions. The update() method lets you simulate follow-up messages, button clicks, and entire conversations in a single test.

Zero Configuration

Works out of the box with #[tokio::test]. No custom thread builders, no special runtime configuration, no port management. Each MockBot gets its own server on a dynamically assigned port.

Full Request Inspection

Access both the resulting message and the original bot request for detailed assertions:

let responses = bot.get_responses();

// Check the message that was sent
let msg = &responses.sent_messages_photo[0].message;
assert_eq!(msg.caption(), Some("Check out this image!"));

// Inspect the raw request your bot made
let request = &responses.sent_messages_photo[0].bot_request;
assert!(request.has_spoiler.unwrap_or(false));

Features

  • Persistent mock server — Server starts once per test, reuses across dispatches
  • Fluent buildersMockMessageText, MockCallbackQuery, MockMessagePhoto, and more
  • Comprehensive API coverage — 40+ Telegram Bot API methods supported
  • Type-safe responses — Dedicated collections for each message type
  • Dependency injection — Full support for dptree::deps![]
  • Dialogue state — Works with InMemStorage, RedisStorage, or any teloxide storage
  • File operations — Mock file uploads, downloads, and media groups
  • Stack-safe — Each dispatch runs in its own tokio task with proper stack isolation

Installation

Add to your Cargo.toml:

[dev-dependencies]
teremock = "0.5"

Quick Start

1. Extract your handler tree

teremock tests your bot by running updates through your handler tree. Extract it into a separate function:

use teloxide::{
    dispatching::{UpdateFilterExt, UpdateHandler},
    prelude::*,
};

type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;

// Your handler function
async fn hello_world(bot: Bot, message: Message) -> HandlerResult {
    bot.send_message(message.chat.id, "Hello World!").await?;
    Ok(())
}

// Extract the handler tree into a function
pub fn handler_tree() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
    dptree::entry()
        .branch(Update::filter_message().endpoint(hello_world))
}

// Use it in your main dispatcher
#[tokio::main]
async fn main() {
    let bot = Bot::from_env();
    Dispatcher::builder(bot, handler_tree())
        .build()
        .dispatch()
        .await;
}

2. Write your tests

#[cfg(test)]
mod tests {
    use teremock::{MockBot, MockMessageText};
    use super::*;

    #[tokio::test]
    async fn test_hello_world() {
        // Create a mock message
        let mock_message = MockMessageText::new().text("Hi!");

        // Create a bot with your handler tree
        let mut bot = MockBot::new(mock_message, handler_tree()).await;

        // Dispatch the update
        bot.dispatch().await;

        // Check the response
        let responses = bot.get_responses();
        let message = responses.sent_messages.last().expect("No messages sent");
        assert_eq!(message.text(), Some("Hello World!"));
    }
}

3. Access detailed request information

For more specific assertions, use typed response collections:

#[tokio::test]
async fn test_with_request_details() {
    let mut bot = MockBot::new(MockMessageText::new().text("/photo"), handler_tree()).await;
    bot.dispatch().await;

    let responses = bot.get_responses();

    // sent_messages_text gives you both the message AND the original request
    let text_response = responses.sent_messages_text.last().unwrap();
    assert_eq!(text_response.message.text(), Some("Here's your photo!"));
    assert_eq!(text_response.bot_request.parse_mode, Some(ParseMode::Html));
}

4. Test multi-step conversations

Use update() to simulate follow-up messages in the same test. The mock server persists between dispatches, so you can test complete user flows:

#[tokio::test]
async fn test_conversation() {
    let mut bot = MockBot::new(MockMessageText::new().text("/start"), handler_tree()).await;

    // First message
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Welcome! Send me a number.")
    );

    // User sends a follow-up
    bot.update(MockMessageText::new().text("42"));
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("You sent: 42")
    );

    // Test callback queries too
    bot.update(MockCallbackQuery::new().data("confirm"));
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Confirmed!")
    );
}

Working with Teloxide Dialogues

If your bot uses teloxide's dialogue system for stateful conversations, teremock has you covered. Just inject your storage as a dependency and test away.

Setting up dialogue tests

use teloxide::{
    dispatching::{dialogue::InMemStorage, UpdateFilterExt, UpdateHandler},
    dptree::deps,
    prelude::*,
};
use teremock::{MockBot, MockMessageText};

#[derive(Clone, Default)]
pub enum State {
    #[default]
    Start,
    AwaitingName,
    AwaitingAge { name: String },
}

#[tokio::test]
async fn test_registration_flow() {
    let mut bot = MockBot::new(MockMessageText::new().text("/start"), handler_tree()).await;

    // Inject the storage — this is the key part for dialogues
    bot.dependencies(deps![InMemStorage::<State>::new()]);

    // Step 1: User sends /start
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Welcome! What's your name?")
    );

    // Step 2: User sends their name
    bot.update(MockMessageText::new().text("Alice"));
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Nice to meet you, Alice! How old are you?")
    );

    // Step 3: User sends their age
    bot.update(MockMessageText::new().text("25"));
    bot.dispatch().await;
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Registration complete! Alice, age 25.")
    );
}

The dialogue state transitions happen naturally through your handler tree — no need to manually set states. This is black-box testing at its finest.

Supported Telegram API Methods

Messages

  • sendMessage, sendPhoto, sendVideo, sendAudio, sendVoice
  • sendVideoNote, sendDocument, sendAnimation, sendSticker
  • sendLocation, sendVenue, sendContact, sendPoll, sendDice
  • sendInvoice, sendMediaGroup, sendChatAction

Editing

  • editMessageText, editMessageCaption, editMessageReplyMarkup

Management

  • deleteMessage, deleteMessages, forwardMessage, copyMessage
  • pinChatMessage, unpinChatMessage, unpinAllChatMessages

Users & Moderation

  • banChatMember, unbanChatMember, restrictChatMember

Callbacks & Commands

  • answerCallbackQuery, setMessageReaction, setMyCommands

Files & Bot Info

  • getFile, getMe, getUpdates, getWebhookInfo

Examples

The examples/ directory contains complete bot implementations with tests:

Example Description
hello_world_bot Simple message handling
calculator_bot Dialogue state machine with callbacks
deep_linking_bot Deep linking with command parameters
album_bot Media group handling
file_download_bot File upload and download operations
phrase_bot Database integration patterns

Database Testing

teremock works seamlessly with database-backed bots. Use your preferred test isolation strategy:

#[tokio::test]
async fn test_with_database() {
    let pool = setup_test_database().await;  // Your test DB setup

    let mut bot = MockBot::new(MockMessageText::new().text("/save hello"), handler_tree()).await;
    bot.dependencies(deps![pool.clone()]);

    bot.dispatch().await;

    // Verify bot response
    assert_eq!(
        bot.get_responses().sent_messages.last().unwrap().text(),
        Some("Saved!")
    );

    // Verify database state
    let saved = sqlx::query!("SELECT phrase FROM phrases")
        .fetch_one(&pool)
        .await
        .unwrap();
    assert_eq!(saved.phrase, "hello");
}

Acknowledgments

teremock builds upon the foundation laid by teloxide_tests by LasterAlex. The original library pioneered the concept of mock testing for teloxide bots.

Key architectural changes in teremock:

  • Persistent server architecture (15-30x faster test execution)
  • Stack-safe dispatch isolation
  • Black-box testing philosophy (no internal state manipulation)
  • Async MockBot::new() for proper initialization

License

MIT License — see LICENSE for details.