Skip to content

TestCases

Reusable testcases for Telegram bot app.

TelegramBotTestCase

Bases: TestCase

Base test case for Telegram bot tests.

Source code in django_telegram_app/bot/testing/testcases.py
class TelegramBotTestCase(TestCase):
    """Base test case for Telegram bot tests."""

    @property
    def webhook_url(self):
        """Return the webhook URL."""
        return reverse("webhook")

    @classmethod
    def tearDownClass(cls):
        """Tear down class-level patches."""
        try:
            super().tearDownClass()
        finally:
            patch.stopall()

    def setUp(self):
        """Set up each test."""
        self.fake_bot_post = patch("django_telegram_app.bot.bot.post", MagicMock()).start()

    def click_on_text(self, text: str, verify: bool = True):
        """Simulate a click on the specified text button."""
        warnings.warn(
            "click_on_text is deprecated and will be removed in version 2. Use click_on_button instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.click_on_button(text, verify=verify)

    def click_on_button(self, button: str | int, verify: bool = True):
        """Simulate a click on an inline button by text or index.

        When `button` is a string, it searches for a button with matching text.
        When `button` is an integer, it treats it as the index of the button in
        the inline keyboard (flattened).
        """
        inline_keyboard = self.fake_bot_post.call_args[1]["payload"]["reply_markup"]["inline_keyboard"]
        if isinstance(button, str):
            callback_data = [item for row in inline_keyboard for item in row if item["text"] == button][0][
                "callback_data"
            ]
        elif isinstance(button, int):
            flat_buttons = [item for row in inline_keyboard for item in row]
            callback_data = flat_buttons[button]["callback_data"]
        else:
            raise ValueError("button must be a string or an integer index")

        data = self.construct_telegram_callback_query(callback_data)
        response = self.post_data(data, verify=verify)
        return response

    def send_text(self, text: str, verify: bool = True):
        """Simulate sending a text message."""
        payload = self.construct_telegram_update(text)
        return self.post_data(payload, verify=verify)

    def post_data(self, data: dict, verify: bool = True):
        """Post data to the webhook."""
        response = self.client.post(
            self.webhook_url,
            data=data,
            headers={"X-Telegram-Bot-Api-Secret-Token": settings.WEBHOOK_TOKEN},
            content_type="application/json",
        )
        if verify:
            self.assertEqual(response.json(), {"status": "ok", "message": "Message received."})
        return response

    @property
    def last_bot_message(self) -> str:
        """Return the last message sent by the bot.

        This is a convenience property for tests and can be used to verify the last message.

        Example:
            self.send_text("/start")
            self.assertEqual(self.last_bot_message, "Welcome to the bot!")
        """
        return self.fake_bot_post.call_args[1]["payload"]["text"]

    @staticmethod
    def construct_telegram_update(message_text: str):
        """Construct a minimal telegram update."""
        return {"message": {"chat": {"id": 123456789}, "text": message_text}}

    @staticmethod
    def construct_telegram_callback_query(callback_data: str):
        """Construct a minimal telegram callback query."""
        return {
            "callback_query": {
                "message": {"message_id": 123, "chat": {"id": 123456789, "first_name": "test", "type": "private"}},
                "data": callback_data,
                "from": {"id": 123456789, "is_bot": False, "first_name": "test"},
            }
        }

last_bot_message property

Return the last message sent by the bot.

This is a convenience property for tests and can be used to verify the last message.

Example

self.send_text("/start") self.assertEqual(self.last_bot_message, "Welcome to the bot!")

webhook_url property

Return the webhook URL.

click_on_button(button, verify=True)

Simulate a click on an inline button by text or index.

When button is a string, it searches for a button with matching text. When button is an integer, it treats it as the index of the button in the inline keyboard (flattened).

Source code in django_telegram_app/bot/testing/testcases.py
def click_on_button(self, button: str | int, verify: bool = True):
    """Simulate a click on an inline button by text or index.

    When `button` is a string, it searches for a button with matching text.
    When `button` is an integer, it treats it as the index of the button in
    the inline keyboard (flattened).
    """
    inline_keyboard = self.fake_bot_post.call_args[1]["payload"]["reply_markup"]["inline_keyboard"]
    if isinstance(button, str):
        callback_data = [item for row in inline_keyboard for item in row if item["text"] == button][0][
            "callback_data"
        ]
    elif isinstance(button, int):
        flat_buttons = [item for row in inline_keyboard for item in row]
        callback_data = flat_buttons[button]["callback_data"]
    else:
        raise ValueError("button must be a string or an integer index")

    data = self.construct_telegram_callback_query(callback_data)
    response = self.post_data(data, verify=verify)
    return response

click_on_text(text, verify=True)

Simulate a click on the specified text button.

Source code in django_telegram_app/bot/testing/testcases.py
def click_on_text(self, text: str, verify: bool = True):
    """Simulate a click on the specified text button."""
    warnings.warn(
        "click_on_text is deprecated and will be removed in version 2. Use click_on_button instead.",
        DeprecationWarning,
        stacklevel=2,
    )
    return self.click_on_button(text, verify=verify)

construct_telegram_callback_query(callback_data) staticmethod

Construct a minimal telegram callback query.

Source code in django_telegram_app/bot/testing/testcases.py
@staticmethod
def construct_telegram_callback_query(callback_data: str):
    """Construct a minimal telegram callback query."""
    return {
        "callback_query": {
            "message": {"message_id": 123, "chat": {"id": 123456789, "first_name": "test", "type": "private"}},
            "data": callback_data,
            "from": {"id": 123456789, "is_bot": False, "first_name": "test"},
        }
    }

construct_telegram_update(message_text) staticmethod

Construct a minimal telegram update.

Source code in django_telegram_app/bot/testing/testcases.py
@staticmethod
def construct_telegram_update(message_text: str):
    """Construct a minimal telegram update."""
    return {"message": {"chat": {"id": 123456789}, "text": message_text}}

post_data(data, verify=True)

Post data to the webhook.

Source code in django_telegram_app/bot/testing/testcases.py
def post_data(self, data: dict, verify: bool = True):
    """Post data to the webhook."""
    response = self.client.post(
        self.webhook_url,
        data=data,
        headers={"X-Telegram-Bot-Api-Secret-Token": settings.WEBHOOK_TOKEN},
        content_type="application/json",
    )
    if verify:
        self.assertEqual(response.json(), {"status": "ok", "message": "Message received."})
    return response

send_text(text, verify=True)

Simulate sending a text message.

Source code in django_telegram_app/bot/testing/testcases.py
def send_text(self, text: str, verify: bool = True):
    """Simulate sending a text message."""
    payload = self.construct_telegram_update(text)
    return self.post_data(payload, verify=verify)

setUp()

Set up each test.

Source code in django_telegram_app/bot/testing/testcases.py
def setUp(self):
    """Set up each test."""
    self.fake_bot_post = patch("django_telegram_app.bot.bot.post", MagicMock()).start()

tearDownClass() classmethod

Tear down class-level patches.

Source code in django_telegram_app/bot/testing/testcases.py
@classmethod
def tearDownClass(cls):
    """Tear down class-level patches."""
    try:
        super().tearDownClass()
    finally:
        patch.stopall()