Telegram-bot uchun avtotestlarni qanday yozish kerak
Chuqur4 daqInnovialabs tahririyati
Qisqacha
Handler va FSM qadamlarini Bot API chaqiruvlari mock qilingan holda va soxta update’lar bilan alohida testlang, Telegram test muhitida bir nechta smoke-test qo‘shing va hammasini CI’da ishga tushiring — yiqilgan test deployni to‘xtatsin.
Nimani va qaysi darajada testlash kerak
Bot asosan handlerlar to‘plamidan iborat: ular update oladi va Bot API metodlarini chaqiradi. Shuning uchun tekshiruvlarni darajalarga ajratsangiz, uni testlash oson:
- Handlerlarning unit-testlari — handler funksiyasini xabar mocki bilan to‘g‘ridan-to‘g‘ri chaqirasiz va u nima yuborgani hamda holatni qanday o‘zgartirganini tekshirasiz. Tez, tarmoqsiz.
- Ssenariy testlari — soxta update’larni dispetcher orqali o‘tkazasiz va chiquvchi Bot API chaqiruvlarini ushlab qolasiz. Ular marshrutlash, filtrlar, middleware va FSM o‘tishlarini birgalikda tekshiradi.
- Integratsion testlar — haqiqiy Telegram test muhitida bir nechta ssenariy.
- CI — hammasi har bir deploydan oldin avtomatik ishga tushadi.
Testlarning aksariyati dastlabki ikki darajada bo‘ladi. Ular regressiyalarning asosiy qismini soniyalar ichida ushlaydi.
aiogram’da handlerlar va FSM
Ikki qadamli odatiy forma:
from aiogram import Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
router = Router()
class OrderForm(StatesGroup):
name = State()
phone = State()
@router.message(Command("order"))
async def start_order(message: Message, state: FSMContext):
await state.set_state(OrderForm.name)
await message.answer("Ismingiz nima?")
@router.message(OrderForm.name)
async def process_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(OrderForm.phone)
await message.answer("Telefon raqamingiz?")
Handlerlarga faqat answer metodi bor xabar va FSMContext kerak. Mock va xotiradagi saqlash joyi yetarli (pytest-asyncio bilan):
import pytest
from unittest.mock import AsyncMock
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.base import StorageKey
from aiogram.fsm.storage.memory import MemoryStorage
from bot.handlers import OrderForm, process_name
@pytest.fixture
def state():
key = StorageKey(bot_id=1, chat_id=42, user_id=42)
return FSMContext(storage=MemoryStorage(), key=key)
@pytest.mark.asyncio
async def test_name_step_moves_to_phone(state):
await state.set_state(OrderForm.name)
message = AsyncMock(text="Anna")
await process_name(message, state)
assert await state.get_state() == OrderForm.phone.state
assert (await state.get_data())["name"] == "Anna"
message.answer.assert_awaited_once()
Har bir test yangi saqlash joyini oladi, shuning uchun holatlar testlar o‘rtasida aralashib ketmaydi. Ssenariy testlari uchun haqiqiy Update obyektlarini dispatcher.feed_update(bot, update) ga bering, bot sessiyasini esa so‘rovlarni yubormasdan yozib boradigan soxta sessiya bilan almashtiring.
grammY’da Bot API chaqiruvlarini ushlab qolish
grammY’da API transformers bor — har bir chiquvchi chaqiruvni o‘rab oladigan funksiyalar. Testlarda transformer chaqiruvni yozib oladi va soxta javob qaytaradi, Telegram’ga hech narsa yetib bormaydi:
import { Bot } from "grammy";
import { expect, test } from "vitest";
import { registerHandlers } from "../src/bot";
import { textUpdate } from "./fixtures";
test("/start sends a greeting", async () => {
const bot = new Bot("test-token", {
botInfo: { id: 1, is_bot: true, first_name: "Test", username: "test_bot" } as any,
});
const calls: { method: string; payload: any }[] = [];
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload });
return { ok: true, result: true } as any;
});
registerHandlers(bot);
await bot.handleUpdate(textUpdate("/start"));
expect(calls[0].method).toBe("sendMessage");
expect(calls[0].payload.text).toContain("Salom");
});
botInfo berilgani uchun getMe chaqirilmaydi. Ko‘p qadamli ssenariylar uchun (sessiyalar yoki conversations plagini) har bir testda bitta bot nusxasini saqlang va ketma-ket bir nechta update yuboring, har biridan keyin calls ni tekshiring.
Soxta update fikstiralari
Har bir testda qo‘lda yozilgan JSON tezda shovqinga aylanadi. Kichik fabrikadan foydalaning:
import type { Update } from "grammy/types";
let seq = 1;
export function textUpdate(text: string, userId = 42): Update {
const command = text.startsWith("/")
? { entities: [{ type: "bot_command" as const, offset: 0, length: text.split(" ")[0].length }] }
: {};
return {
update_id: seq++,
message: {
message_id: seq++,
date: Math.floor(Date.now() / 1000),
chat: { id: userId, type: "private", first_name: "Test" },
from: { id: userId, is_bot: false, first_name: "Test" },
text,
...command,
},
};
}
Vaqtni tejaydigan qoidalar:
- Buyruqlar uchun entities qo‘shing — buyruq filtrlari ularga tayanishi mumkin.
- Ishlov beradigan har bir update turi uchun fabrika yarating: callback query, kontaktlar, rasmlar, guruhdagi xabarlar.
- Asos sifatida loglardagi haqiqiy update’larni oling, shaxsiy ma’lumotlarni olib tashlab.
Test serverdagi integratsion testlar
Telegram’da o‘z akkauntlari va o‘z BotFather’i bo‘lgan alohida test muhiti bor. U yerda yaratilgan bot faqat shu muhitda ishlaydi. Bot API so‘rovlari https://api.telegram.org/bot<token>/test/METHOD_NAME manziliga yuboriladi; grammY’da bu client: { environment: "test" } opsiyasi, boshqa freymvorklar bazaviy URL’ni o‘zgartirishga imkon beradi.
Undan bir nechta smoke-ssenariy uchun foydalaning: bot ishga tushadi, webhook qabul qilinadi, haqiqiy formatlash va klaviaturalaringiz bilan xabarlar validatsiyadan o‘tadi. Foydalanuvchi nomidan harakat qilish uchun test akkauntida avtorizatsiyadan o‘tgan MTProto-klient xabar yuborib, javob kutishi mumkin. Test muhiti ma’lumotlarni saqlashni va’da qilmaydi, shuning uchun testlar o‘zlariga kerakli hamma narsani o‘zlari yaratishi kerak.
CI’da ishga tushirish
name: bot
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: pytest -q
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
needs: test qizil test deployni to‘xtatishini bildiradi. Integratsion testlarni secrets’dagi test-bot tokeni bilan alohida job’da saqlang va unit-testlarda hech qachon production tokenidan foydalanmang.