Skip to content

Add TransitionTesting class

Сергей Ряпин requested to merge transitionTesting into master

Добавлены варианты для создания интеграциионного тестирования переходов между состояниями.

1 Вариант

Порядок создания теста:

  1. Cоздать класс, унаследованный от класса ChainTesting.
  2. В новом классе переопределить 2 метода: add_responses, check_one_step.
  3. Создать переменную chain_state_transitions: list[Chain], необходимую для создания экземпляра класса.
  4. Создать экземпляр созданного класса с атрибутом chain_state_transitions.
  5. Вызвать метод go_through_states(httpx_mock) у созданного экземпляра внутри функции для тестирования.

Метод add_responses

В данном методе необходимо создать все запросы, и ответы на них, которые всречаются в цепоче переходов при тестировании:

httpx_mock.add_response(
    url=<url запроса>,
    method=...,
    content=<ответ в виде сырых данных>,
)

Метод check_one_step

Содержит механизм проверки фактически достигнутого состояния на равенство finished_locator и равенство фактических параметров состояния для одного перехода. В дальнейшем, после того как модуль state_machine_runners полностью перейдет во фрэймворк, этот метод тоже можно будет перенести полностью в базовый класс.

Переменная chain_state_transitions

Должна содержать список объектов типа Chain. Последовательность объектов должна соответствовать цепочке переходов между состояниями. Первый объект должен соответствовать состоянию первого обращения пользователя к боту, т.е.:

Chain(msg='/start', finished_locator=<конечный локатор для данного события>)

Объект Chain

Описывает тригерное событие (msg - сообщение, либо сallback_data - нажатие инлайн кнокпи) и то состояние (finished_locator и проверяемые параметры конечного состояния state_params), в котором должен оказаться бот после этого. state_params не является обязательным для проверки состояния и может не указываться, при этом проверка параметров состояния не будет проводиться.

Пример использования для бота Proxys:


import pytest
import json

from django.test import override_settings
from django.conf import settings
from pytest_httpx import HTTPXMock
from urllib.parse import urljoin

from tg_api import Update
from ..states import router
from ..state_machine_runners import process_tg_update
from ..models import conversation_var, Conversation
from . import responses
from .responses import Chain


TEST_API_KEY = 'fdf25258d5d8a7fd87ee824dfdf35fff'


class TgBotTest(responses.ChainTesting):

    def add_responses(self, httpx_mock: HTTPXMock) -> None:
        tg_client_api_root = urljoin('https://api.telegram.org', f'./bot{settings.ENV.TG.BOT_TOKEN}/')
        httpx_mock.add_response(
            url=f'{tg_client_api_root}sendMessage',
            method='POST',
            content='{"ok": "true", "result": {"message_id": 1, "date": 1, "chat": {"id": 1, "type": "Test"}}}',
        )
        httpx_mock.add_response(
            url=f'https://proxys.io/ru/api/v2/check-api-key?key={TEST_API_KEY}',
            method='GET',
            content='{"success": "true", "data": {"email": "test@test.ru"}}'
        )
        httpx_mock.add_response(
            url=f'https://proxys.io/ru/api/v2/balance?key={TEST_API_KEY}',
            method='GET',
            content='{"success": "true", "data": {"user_balance": 1600, "currency": "RUB", "buyCourse": 10}}'
        )
        httpx_mock.add_response(
            url='http://proxys.io/ru/api/v2/services?tariff=1&description=0',
            method='GET',
            content=json.dumps(responses.services_response)
        )
        httpx_mock.add_response(
            url=f'http://proxys.io/ru/api/v2/ip?key={TEST_API_KEY}',
            method='GET',
            content=json.dumps(responses.ip_response)
        )

    def check_one_step(self, update: Update, state_params: dict | None = None, finished_locator: str = '/') -> None:
        process_tg_update(update, router=router, conversation_var=conversation_var)
        updated_conversation = Conversation.objects.get(tg_chat_id=1)
        assert updated_conversation.state_class_locator == finished_locator
        if state_params:
            assert updated_conversation.state_params == state_params


chain_states = [
    Chain(msg='/start', finished_locator='/welcome/'),
    Chain(msg=TEST_API_KEY, finished_locator='/main-menu/'),
    Chain(msg='/lang', finished_locator='/language-switcher/'),
    Chain(
        msg='English (🇺🇸)',
        state_params={'forced_language_code': 'en'},
        finished_locator='/main-menu/',
    ),
    Chain(msg='/exit', finished_locator='/welcome/'),
    Chain(msg=TEST_API_KEY, finished_locator='/main-menu/'),
    Chain(msg='💳 Buy proxy', finished_locator='/buy-proxy/'),
    Chain(
        callback_data='/buy-proxy/country-selection/#5#Individual IPv4',
        state_params={
            'service_id': 5,
            'forced_language_code': None,
            'service_name': 'Individual IPv4 (foreign)',
        },
        finished_locator='/buy-proxy/country-selection/'
    ),
    Chain(msg='/start', finished_locator='/main-menu/'),
]


@override_settings(ROLLBAR=None)
@pytest.mark.django_db
@pytest.mark.anyio()
def test_go_through_states(httpx_mock: HTTPXMock):
    TgBotTest(
        chain_state_transitions=chain_states
    ).go_through_states(httpx_mock)

2 Вариант

Порядок создания теста:

  1. Cоздать переменную responses: list[MockResponse].
  2. Создать переменную state_chain: list[Step], необходимую для создания экземпляра класса.
  3. Создать экземпляр класса с TransitionTesting.
  4. Вызвать метод go_through_states(httpx_mock) у созданного экземпляра внутри функции для тестирования.

Объект MockResponse

Необходим для создания подменного запроса функцией httpx_mock.add_response. Используется для создания переменной responses, которая используется в методе go_state_chain:

    def go_state_chain(self, httpx_mock: HTTPXMock) -> None:
        for response in self.responses:
            httpx_mock.add_response(
                **response.dict(exclude={'json_', 'matchers'}),
                **response.matchers,
                json=response.json,
            )
        ...

Объект Step

Описывает тригерное событие (msg - сообщение, либо сallback_data - нажатие инлайн кнокпи) и то состояние (finished_locator и проверяемые параметры конечного состояния state_params), в котором должен оказаться бот после этого. state_params не является обязательным для проверки состояния и может не указываться, при этом проверка параметров состояния не будет проводиться.

класс TransitionTesting

Содержит атрибуты и методы для создания теста. Каждый экземпляр класса позволяет провести тестирование определенной последовательности переходов, которая описывается атрибутом state_chain: list[Step], для определенного router. Для создания экземпляра необходимы следующие переменные:

    responses: list[MockResponse]
    state_chain: list[Step]
    process_tg_update: Callable[..., None]
    router: Router
    conversation_var: Any
    conversation_model: Type[models.Model]

Пример использования для бота Proxys:

import json
import pytest

from urllib.parse import urljoin
from django.conf import settings
from pytest_httpx import HTTPXMock
from django.test import override_settings

from . import responses
from .responses import MockResponse, Step, TransitionTesting
from ..states import router
from ..state_machine_runners import process_tg_update
from ..models import conversation_var, Conversation

TEST_API_KEY = 'fdf25258d5d8a7fd87ee824dfdf35fff'
TG_API_ROOT = urljoin('https://api.telegram.org', f'./bot{settings.ENV.TG.BOT_TOKEN}/')

responses = [
    MockResponse(
        url=f'{TG_API_ROOT}sendMessage',
        method='POST',
        content='{"ok": "true", "result": {"message_id": 1, "date": 1, "chat": {"id": 1, "type": "Test"}}}',
    ),
    MockResponse(
        url=f'https://proxys.io/ru/api/v2/check-api-key?key={TEST_API_KEY}',
        method='GET',
        content='{"success": "true", "data": {"email": "test@test.ru"}}',
    ),
    MockResponse(
        url=f'https://proxys.io/ru/api/v2/balance?key={TEST_API_KEY}',
        method='GET',
        content='{"success": "true", "data": {"user_balance": 1600, "currency": "RUB", "buyCourse": 10}}',
    ),
    MockResponse(
        url='http://proxys.io/ru/api/v2/services?tariff=1&description=0',
        method='GET',
        content=json.dumps(responses.services_response),
    ),
    MockResponse(
        url=f'http://proxys.io/ru/api/v2/ip?key={TEST_API_KEY}',
        method='GET',
        content=json.dumps(responses.ip_response),
    )
]


state_chain = [
    Step(msg='/start', locator='/welcome/'),
    Step(msg=TEST_API_KEY, locator='/main-menu/'),
    Step(msg='/lang', locator='/language-switcher/'),
    Step(
        msg='English (🇺🇸)',
        state_params={'forced_language_code': 'en'},
        locator='/main-menu/',
    ),
    Step(msg='/exit', locator='/welcome/'),
    Step(msg=TEST_API_KEY, locator='/main-menu/'),
    Step(msg='💳 Buy proxy', locator='/buy-proxy/'),
    Step(
        callback_data='/buy-proxy/country-selection/#5#Individual IPv4',
        state_params={
            'service_id': 5,
            'forced_language_code': None,
            'service_name': 'Individual IPv4 (foreign)',
        },
        locator='/buy-proxy/country-selection/'
    ),
    Step(msg='/start', locator='/main-menu/'),
]


@override_settings(ROLLBAR=None)
@pytest.mark.django_db
@pytest.mark.anyio()
def test_go_through_states(httpx_mock: HTTPXMock):
    TransitionTesting(
        responses=responses,
        state_chain=state_chain,
        process_tg_update=process_tg_update,
        router=router,
        conversation_var=conversation_var,
        conversation_model=Conversation,
    ).go_state_chain(httpx_mock)

3 Вариант

Порядок создания теста:

  1. Cоздать класс, унаследованный от класса ImperativeTransitionTesting.
  2. Переопределить метод create_state_chain. В методе последовательно вызвать метод self.check_step в соответствии с порядком переходов между состояниями.
  3. Cоздать переменную responses: list[MockResponse].
  4. Создать экземпляр созданного класса.
  5. Вызвать метод go_state_chain(httpx_mock) у созданного экземпляра внутри функции для тестирования.

Класс ImperativeTransitionTesting

Класс используется как родительский класс. В дочернем классе необходимо переопределить метод create_state_chain. Экземпляр дочернего класса позволяет провести тестирование определенной последовательности переходов, которая описывается в методе create_state_chain, для определенного router.

Для создания экземпляра необходимы следующие переменные:

    responses: list[MockResponse]
    router: Router
    conversation_var: Any
    conversation_model: Type[models.Model]
    process_tg_update: Callable[..., None]

Объект MockResponse

Необходим для создания подменного запроса функцией httpx_mock.add_response. Используется для создания переменной responses, которая используется в методе go_state_chain:

    def go_state_chain(self, httpx_mock: HTTPXMock) -> None:
        for response in self.responses:
            httpx_mock.add_response(
                **response.dict(exclude={'json_', 'matchers'}),
                **response.matchers,
                json=response.json,
            )
        ...

Пример использования для бота Proxys:

import json
import pytest

from urllib.parse import urljoin
from django.conf import settings
from pytest_httpx import HTTPXMock
from django.test import override_settings

from . import responses
from .responses import MockResponse, ImperativeTransitionTesting
from ..states import router
from ..state_machine_runners import process_tg_update
from ..models import conversation_var, Conversation

TEST_API_KEY = 'fdf25258d5d8a7fd87ee824dfdf35fff'
TG_API_ROOT = urljoin('https://api.telegram.org', f'./bot{settings.ENV.TG.BOT_TOKEN}/')


class TgBotChainTest(ImperativeTransitionTesting):

    def create_state_chain(self):
        self.check_step(msg='/start', finished_locator='/welcome/')
        self.check_step(msg=TEST_API_KEY, finished_locator='/main-menu/')
        self.check_step(msg='/lang', finished_locator='/language-switcher/')
        self.check_step(
            msg='English (🇺🇸)',
            state_params={'forced_language_code': 'en'},
            finished_locator='/main-menu/',
        )
        self.check_step(msg='/exit', finished_locator='/welcome/')
        self.check_step(msg=TEST_API_KEY, finished_locator='/main-menu/')
        self.check_step(msg='💳 Buy proxy', finished_locator='/buy-proxy/')
        self.check_step(
            callback_data='/buy-proxy/country-selection/#5#Individual IPv4',
            state_params={
                'service_id': 5,
                'forced_language_code': None,
                'service_name': 'Individual IPv4 (foreign)',
            },
            finished_locator='/buy-proxy/country-selection/'
        )
        self.check_step(msg='/start', finished_locator='/main-menu/')


mock_responses = [
    MockResponse(
        url=f'{TG_API_ROOT}sendMessage',
        method='POST',
        content='{"ok": "true", "result": {"message_id": 1, "date": 1, "chat": {"id": 1, "type": "Test"}}}',
    ),
    MockResponse(
        url=f'https://proxys.io/ru/api/v2/check-api-key?key={TEST_API_KEY}',
        method='GET',
        content='{"success": "true", "data": {"email": "test@test.ru"}}',
    ),
    MockResponse(
        url=f'https://proxys.io/ru/api/v2/balance?key={TEST_API_KEY}',
        method='GET',
        content='{"success": "true", "data": {"user_balance": 1600, "currency": "RUB", "buyCourse": 10}}',
    ),
    MockResponse(
        url='http://proxys.io/ru/api/v2/services?tariff=1&description=0',
        method='GET',
        content=json.dumps(responses.services_response),
    ),
    MockResponse(
        url=f'http://proxys.io/ru/api/v2/ip?key={TEST_API_KEY}',
        method='GET',
        content=json.dumps(responses.ip_response),
    )
]


@override_settings(ROLLBAR=None)
@pytest.mark.django_db
@pytest.mark.anyio()
def test_go_through_states(httpx_mock: HTTPXMock):
    TgBotChainTest(
        responses=mock_responses,
        process_tg_update=process_tg_update,
        router=router,
        conversation_var=conversation_var,
        conversation_model=Conversation,
    ).go_state_chain(httpx_mock)
Модуль тестирования:
  Вариант 1: !concept
    Создание теста: !example |
      Программист определяет различные сценарии работы бота для тестирования
      Программист определяет последовательность переходов между состояниями бота для каждого сценария в специальных переменных
      Программист создаёт класс унаследованный от специального класса
      Программист определяет необходимые подменные запросы переопределяя специальный метод родительского класса
      Программист определяет логику тестирования одного перехода состояния переопределяя специальный метод родительского класса
      Для каждого сценария тестирования программист создаёт свой класс либо все сценарии прописывает в одном классе
      Создаёт экземпляры классов и вызывает специальный метод внутри функции для тестирования
  Вариант 2: !concept
    Создание теста: !example |
      Программист определяет различные сценарии работы бота для тестирования
      Программист определяет последовательность переходов между состояниями бота для каждого сценария в специальных переменных
      Программист определяет необходимые подменные запросы и кладет их в специальную переменную
      Для каждого сценария работы программист создаёт свой экземпляр специального класса
      У созданных экземпляров вызывает специальный метод родительского класса внутри функции для тестирования
    Добавить дополнительные проверки: ! extension |
      Переопределить специальный метод родительского класса, отвечающий за тестирование одного шага, добавив в него необходимые проверки
  Вариант 3: !concept
    Создание теста: example |
      Программист определяет различные сценарии работы бота для тестирования
      Программист создаёт класс унаследованный от специального класса
      Программист определяет последовательность переходов между состояниями бота для каждого сценария, переопределяя специальный метод родительского класса
      Программист определяет необходимые подменные запросы и кладет их в специальную переменную
      Для каждого сценария работы программист создаёт свой класс и экземпляр
      У созданных экземпляров вызывает специальный метод внутри функции для тестирования
    Добавить дополнительные проверки: ! extension |
      Переопределить специальный метод родительского класса, отвечающий за тестирование одного шага, добавив в него необходимые проверки
      В методе, определяющем последовательность переходов, добавить необходимые проверки
Edited by Сергей Ряпин

Merge request reports