Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот

в 10:54, , рубрики: api, python, qa automation, платежные системы, Разработка под e-commerce, Тестирование веб-сервисов

image

Предисловие

Наша команда разрабатывает финансовые инструменты, в том числе открытые платежные API, и как многие проекты, работающие по практике continuous integration мы одновременно с созданием проекта 3 года назад начали думать над тем, как улучшить покрытие проекта тестами и добиться максимальной стабильности нашего кода при довольно частых изменениях (мы иногда устанавливаем обновления на продуктовую среду несколько раз в день). Особенно это важно в трех аспектах:

  • мы предоставляем наши API интерфейсы в открытый доступ клиентам и важно, чтобы все взаимодействие четко соответствовало описаниям спецификаций
  • мы интегрируемся с большим количеством других финансовых сервисов и банков, и помимо покрытия тестами своего кода мы вынуждены также покрывать интеграционными тестами взаимодействие с test (а иногда и prod) средой сторонних систем
  • наша внутренняя архитектура включает в себя большое количество микросервисов, которые общаются между собой по HTTP API

В этой статье я хотел бы поделиться опытом и показать пример, как мы разрабатываем тесты для API интерфейсов включающих в себя как сервер-сервер взаимодействие, так и работу через браузер.  Для демонстрации я приведу простой пример тестирования процесса оплаты банковской картой через наш платежный шлюз с отправкой результата тестов в Telegram.

Проект с примерами тестов вы можете скачать в GitHub: https://github.com/cloudipsp/auto_tests.git

Документация по нашему тестируемому платежному API: https://www.fondy.eu/info/api/

Подготавливаем среду

Для разработки тестов мы используем Robot Framework, и хотя у этого фреймворка есть собственная среда разработки RIDE, но она существенно уступает PyCharm по удобству и возможностям

RIDE
image

Для начала разработки установим 

  1. virtualenv
    pip install virtualenv setuptools
    
  2. бесплатную версию PyCharm Edu
    https://www.jetbrains.com/pycharm-edu/download/
  3. для PyCharm ставим плагины
    intellibot: 
    http://plugins.jetbrains.com/plugin/7386?pr=pycharm111

    Robot Framework Support:
    http://plugins.jetbrains.com/plugin/7415?pr=pycharm99

    при этом для актуальной версии PyCharm Edu 2.0.4 мне пришлось ставить версию robot framework 0.14.2, так как последняя 0.15 оказалась не совместимой

  4. клонируем проект с github — этот пункт можно пропустить, если есть желание проделать все с нуля:
    git clone https://github.com/cloudipsp/auto_tests.git
    

  5. устанавливаем зависимости:

    для начала нам достаточно таких библиотек:

    robotframework==2.9a1
    selenium
    robotframework-selenium2library
    requests

    Создаем файл pip-requires.txt с этим содержимым, активируем virtualenv и устанавливаем

    cd auto_tests
    pip install -r pip-requires.txt
    

Разработка: автотесты без браузера

Для примера возьмем тип покупки по 3DSecure карте, когда карта вводится на стороне торговца (раздел API https://www.fondy.eu/ru/info/api/v1.0/4).

Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот - 3

Для простоты исключим Шаг 2. — редирект в браузере (мы его протестируем в следующем примере). Для тестовой карты этот редирект происходит на страницу — эмулятор банковской, которая всегда возвращает один и тот же результат — пароль введен верно.

В этом случае у нас будет 2 шага:

  • шаг 1 — отправка на API https://api.fondy.eu/api/3dsecure_step1/ тестовых данных платежа и реквизитов карты и получение в ответе адреса URL страницы, куда необходимо перенаправить клиента (параметры response_status, acs_url, pareq, md ) для ввода 3DSecure пароля (в случае тестовых данных никакой пароль запрашиваться не будет, вместо страницы эмитента будет страница-заглушка)
  • шаг 3 — отправляем данные order_id, merchant_id, pares, md, version, signature на API https://api.fondy.eu/api/3dsecure_step2/ , получаем финальный ответ и сравниваем его со спецификациями

Первоначальные файлы настроек

В документации по ссылке https://www.fondy.eu/ru/info/api/v1.0/2 возьмем тестовые карты на которых и будем тестировать. Кстати все тестовые платежи можно будет увидеть в демо-кабинете торговца: https://www.fondy.eu/mportal/#/account/demo
Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот - 4

Теперь создадим файлы робота:

cards.robot

*** Settings ***
Documentation     A resource file with test credit cards. Imported once in resource.robot

*** Variables ***
#Name                         Card Number              Exp Month     Exp Year       Cvv2
@{3dsApproved}                  4444555566661111    01                  24                 238
@{no3dsApproved}              4444555511116666    01                  24                 238
@{3dsDeclined}                   4444111166665555    01                  24                 238
@{no3dsDeclined}               4444111155556666    01                  24                 238

merchant.robot

*** Settings ***
Documentation     A resource file with test merchants. Imported once in resource.robot

*** Variables ***
${TestMerchant}           1397120    #(Test merchant)

variables.robot

*** Settings ***
Documentation     Variables used in all tests. Imported one time in resource.robot

*** Variables ***
${API SERVER}               api.fondy.eu
${JSON}                     application/json
${XML}                      application/xml
${FORM}                     application/x-www-form-urlencoded

resource.robot

*** Settings ***
Documentation     A resource file with reusable keywords.

Resource          variables.robot
Resource          cards.robot
Resource          merchants.robot
Library           helper/utils.py
Library           requester.py

Спецификации протоколов

Для того, чтобы быть уверенным, что запрос к API и ответ от него соответствует спецификациям, создадим файл specifications_settings.py, который будет содержать структуру параметров, описанных в документации. Например параметры в документации
Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот - 5

будут соответствовать структуре

PAY_SERVER2SERVER_3DS = {
    'request_step1': {
        "order_id": {
            "required": True,
            "type": "string",
            "size": 1024
        },
        "merchant_id": {
            "required": True,
            "type": "int",
            "size": 12
        },
        "order_desc": {
            "required": True,
            "type": "string",
            "size": 1024
        },
 

полный specifications_settings.py

PAY_SERVER2SERVER_3DS = {
    'request_step1': {
        "order_id": {
            "required": True,
            "type": "string",
            "size": 1024
        },
        "merchant_id": {
            "required": True,
            "type": "int",
            "size": 12
        },
        "order_desc": {
            "required": True,
            "type": "string",
            "size": 1024
        },
        "signature": {
            "required": True,
            "type": "string",
            "size": 40
        },
        "amount": {
            "required": True,
            "type": "amount",
            "size": 12
        },
        "currency": {
            "required": True,
            "type": "string",
            "size": 3
        },
        "version": {
            "default": "1.0",
            "required": False,
            "type": "string",
            "size": 10
        },
        "server_callback_url": {
            "required": False,
            "type": "url",
            "size": 2048
        },
        "lifetime": {
            "required": False,
            "type": "int",
            "size": 6
        },
        "merchant_data": {
            "required": False,
            "type": "string",
            "size": 2048
        },
        "preauth": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "sender_email": {
            "required": False,
            "type": "email",
            "size": 50
        },
        "lang": {
            "required": False,
            "type": "string",
            "size": 2
        },
        "product_id": {
            "required": False,
            "type": "string",
            "size": 1024
        },
        "verification": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "card_number": {
            "required": True,
            "type": "string",
            "size": 19
        },
        "cvv2": {
            "required": True,
            "type": "string",
            "size": 3
        },
        "expiry_date": {
            "required": True,
            "type": "date",
            "size": 4,
            "important": False,
        },
    },
    'request_step2': {
        "order_id": {
            "required": True,
            "type": "string",
            "size": 1024
        },
        "merchant_id": {
            "required": True,
            "type": "int",
            "size": 12
        },
        "pares": {
            "required": True,
            "type": "string",
            "size": 20480
        },
        "md": {
            "required": True,
            "type": "string",
            "size": 1024
        },
        "version": {
            "default": "1.0",
            "required": False,
            "type": "string",
            "size": 10
        },
        "signature": {
            "required": True,
            "type": "string",
            "size": 40
        },
    },
    'response_3ds': {
        "response_status": {
            "type": "string",
            "required": True,
            "size": 50
        },
        "acs_url": {
            "type": "string",
            "required": True,
            "size": 2048
        },
        "pareq": {
            "type": "string",
            "required": True,
            "size": 20480
        },
        "md": {
            "default": "",
            "type": "string",
            "required": True,
            "description_en": "",
            "description_ru": "",
            "size": 1024
        },
    },
    'response_final': {
        "order_id": {
            "type": "string",
            "size": 1024
        },
        "merchant_id": {
            "type": "int",
            "size": 12
        },
        "amount": {
            "type": "amount",
            "size": 12
        },
        "currency": {
            "type": "string",
            "size": 3
        },
        "order_status": {
            "type": "string",
            "size": 50
        },
        "response_status": {
            "type": "string",
            "size": 50
        },
        "signature": {
            "type": "string",
            "size": 40
        },
        "tran_type": {
            "type": "string",
            "size": 50
        },
        "sender_cell_phone": {
            "type": "string",
            "size": 20
        },
        "sender_account": {
            "type": "string",
            "size": 50
        },
        "masked_card": {
            "type": "string",
            "size": 19
        },
        "card_bin": {
            "type": "int",
            "size": 6
        },
        "card_type": {
            "type": "string",
            "size": 50
        },
        "rrn": {
            "type": "string",
            "size": 50
        },
        "approval_code": {
            "type": "string",
            "size": 6
        },
        "response_code": {
            "type": "int",
            "size": 4
        },
        "response_description": {
            "type": "string",
            "size": 1024
        },
        "reversal_amount": {
            "type": "amount",
            "size": 12
        },
        "settlement_amount": {
            "type": "amount",
            "size": 12
        },
        "settlement_currency": {
            "type": "string",
            "size": 3
        },
        "order_time": {
            "type": "time",
            "size": 19
        },
        "settlement_date": {
            "type": "time",
            "size": 10
        },
        "eci": {
            "type": "string",
            "size": 2
        },
        "fee": {
            "type": "amount",
            "size": 12
        },
        "payment_system": {
            "type": "string",
            "size": 50
        },
        "sender_email": {
            "type": "email",
            "size": 254
        },
        "payment_id": {
            "type": "int",
            "size": 19
        },
        "actual_amount": {
            "type": "amount",
            "size": 12
        },
        "actual_currency": {
            "type": "string",
            "size": 3
        },
        "product_id": {
            "type": "string",
            "size": 1024
        },
        "merchant_data": {
            "type": "string",
            "size": 2048,
        },
        "verification_status": {
            "type": "string",
            "size": 48,
        },
        "rectoken": {
            "type": "string",
            "size": 48,
        },
        "rectoken_lifetime": {
            "type": "time",
            "size": 19,
        },
    },
}

Далее создаем функции

функция, которая будет пробегать по файлу спецификаций specifications_settings.py и создавать запрос в формате JSON, XML, FORM из набор всех данных разных типов, добивая их до максимальной длины.

Build required parameters dict

   def build_required_parameters_dict(self, merchant_id, currency, spec, spec_dict, response_url=None, *args,
                                       **kwargs):
        self.merchant_id = merchant_id
        request_params_specs = getattr(
            specifications_settings, spec)[spec_dict]
        # for requests with cards
        if args:
            kwargs['card_number'] = args[0]
            kwargs['expiry_date'] = int(str(args[1]) + str(args[2]))
            kwargs['cvv2'] = args[3]
        request_params = {}
        for param in request_params_specs:
            if param in kwargs.iterkeys():
                request_params[param] = kwargs[param]
            elif param == "signature":
                request_params[param] = ''
            elif param == "currency":
                request_params[param] = currency
            elif param == "payment_systems":
                request_params[param] = 'card'
            elif param == "response_url":
                request_params[param] = response_url
            elif param == "merchant_id":
                request_params[param] = merchant_id
            elif param == "delayed":
                request_params[param] = "n"
            elif param == "order_desc":
                request_params[param] = 'test' + randomStr(size=7, chars=string.digits)
            elif param == "order_id":
                request_params[param] = self.order_id
            # for 3ds requests
            elif param == "pares":
                request_params[param] = TEST_PARES
            elif param == "md":
                request_params[param] = self.md
            # any other parameters
            elif request_params_specs[param]["type"] == "email":
                request_params[param] = "test@fondy.eu"
            elif request_params_specs[param]["type"] == "string":
                request_params[param] = randomStr(
                    request_params_specs[param]["size"], param).encode('utf-8')
            elif request_params_specs[param]["type"] == "url":
                request_params[param] = "https://" + randomStr(
                    request_params_specs[param]["size"] - 12, param).encode('utf-8') + ".com"
            elif request_params_specs[param]["type"] == "int":
                request_params[param] = randomStr(request_params_specs[param]["size"], "",
                                                  string.digits)
            elif request_params_specs[param]["type"] == "amount":
                request_params[param] = randomStr(
                    5, "", string.digits)
            elif request_params_specs[param]["type"] == "boolean":
                request_params[param] = randomStr(
                    1, "", "YN")

        self.request_params = request_params

функцию непосредственной HTTPS POST отправки данных на API:

Send request

    def send_request(self, content_type, url=None, data=None, protocol=False, **kwargs):
        requests.packages.urllib3.disable_warnings()
        print "*HTML* sending request"
        print "*HTML* content_type=%r, url=%r, data=%r, kwargs=%r" % (content_type, url, data, kwargs)
        if data is None:
            data = self.request_params
        if self.order_id == '':
            data['order_id'] = 'test' + randomStr(
                10, "", string.ascii_letters)
        else:
            data['order_id'] = self.order_id

        data['signature'] = ""
        data['signature'] = build_signature(self.request_params)
        self.save_order_id_from_server(data['order_id'])
        post_data = self.build_request(content_type, data)
        print "*HTML* POSTREQUEST  %s" % (post_data)
        self.response = requests.post(
            url, headers={'Content-Type': content_type}, data=post_data, verify=False).text
        print "*HTML* POSTRESPONSE  %s" % (self.response)
        return self.response

также нам нужна функция для проверки ответа от API, которая сверит все полученные параметры с файлом спецификаций specifications_settings.py:

Verify response status

    def verify_response_status(self, spec, spec_dict, content_type, response=None, request_params=None,
                               status='approved'):
        try:
            if response == None:
                response = self.response
            print "*HTML* response  %s" % (response)
            if request_params == None:
                if self.request_params:
                    request_params = self.request_params
            print "*HTML* REq_par  %s" % (request_params)
            response_params_specs = getattr(
                specifications_settings, spec)[spec_dict]
            print "*HTML* REsponse_par_spec  %s" % (response_params_specs)
            errors_list = []
            error = False
            response_params = parse_response(self.response, content_type)
            print "*HTML* REsp_par  %s" % (response_params)
            for param in response_params_specs:
                if response_params[param] is not None:
                    if response_params_specs[param]["type"] == "string":
                        if len(response_params[param]) > response_params_specs[param]["size"]:
                            errors_list.append('Error: size of param ' + param + ' is ' + str(
                                len(response_params[param])) + ' but max is ' + str(
                                response_params_specs[param]["size"]))
                            error = True
                    elif response_params_specs[param]["type"] == "int":
                        if len(str(response_params[param])) > response_params_specs[param]["size"]:
                            errors_list.append('Error: size of param ' + param + ' is ' + str(
                                len(str(response_params[param]))) + ' but max is ' + str(
                                response_params_specs[param]["size"]))
                            error = True
                        if response_params[param] != "" and not str(response_params[param]).isdigit():
                            errors_list.append(
                                'Error: param ' + param + ' is not integer')
                            error = True
                else:
                    errors_list.append('Error: param ' + param + ' is missing')
                    error = True
                if request_params.get(param) is not None and request_params.get(
                        param) != "" and param != 'signature' and response_params.get(param) is not None:
                    if (response_params_specs[param]["type"] == "string" and request_params.get(
                            param) != response_params.get(
                        param)) or (
                                    response_params_specs[param]["type"] == "amount" and int(
                                request_params.get(param)) != int(
                                response_params.get(param))):
                        request = 'request:' + str(request_params.get(param))
                        response = 'response:' + str(response_params.get(param))
                        order_id = 'order_id:' + 
                                   str(response_params.get('order_id'))
                        errors_list.append(
                            'Error: param ' + param + ' is not equal in request and '
                                                      'responsen request=%sn response=%s order_id=%s' % (
                                request, response, order_id))
                        error = True

            if response_params_specs.get('signature') is not None:
                params_sign = {param: response_params.get(param, "") for param in response_params_specs if
                               param != 'signature'}
                params = collections.OrderedDict(sorted(response_params.items()))
                params_sign['signature'] = build_signature(params_sign)
                if params_sign['signature'] != params["signature"]:
                    errors_list.append('Error: signature invalid in response ')
                    error = True

            if response_params.get('order_status') and response_params.get('order_status') != status:
                errors_list.append('Error: invalid status in response ')
                error = True
        except Exception as e:
            errors_list.append("final %s" % e.message)
            error = True
        finally:
            if error:
                raise Exception("*HTML* Errors:n %s" % errors_list)
            else:
                print "*HTML* test passed OK"

и последнюю функцию сохранения ответа от API на шаге 1 для передачи параметров на шаг 2

Save md pareq and acs url for 3ds

    def save_order_id_from_server(self, order_id):
        self.order_id = order_id
        print "*HTML* Order_id %s" % (self.order_id)


Теперь на базе этих функций мы можем построить тестовый сценарий pay_with_3ds_card.robot

*** Settings ***
Documentation     A test suite containing tests related to server-server complete purchase with 3ds card.

Test Template     Server-server full purchase with 3ds card Should Pass
Test Timeout      15 seconds
Default Tags      smoke    3ds
Library           DebugLibrary
Resource          ../resource.robot

*** Variables ***
${specificatons}        PAY_SERVER2SERVER_3DS
${req_dict_step1}       request_step1
${resp_dict_step1}      response_3ds
${url_step1}            https://${API SERVER}/api/3dsecure_step1/
${req_dict_step2}       request_step2
${resp_dict_step2}      response_final
${url_step2}            https://${API SERVER}/api/3dsecure_step2/

***Test Cases ***        merchant_id           currency    content_type    credit_card
USD_JSON_Approved        ${TestMerchant}       USD         ${JSON}         @{3dsApproved}
USD_XML_Approved         ${TestMerchant}       USD         ${XML}          @{3dsApproved}
USD_FORM_Approved        ${TestMerchant}       USD         ${FORM}         @{3dsApproved}
UAH_JSON_Approved        ${TestMerchant}       UAH         ${JSON}         @{3dsApproved}
UAH_XML_Approved         ${TestMerchant}       UAH         ${XML}          @{3dsApproved}
UAH_FORM_Approved        ${TestMerchant}       UAH         ${FORM}         @{3dsApproved}
EUR_JSON_Approved        ${TestMerchant}       EUR         ${JSON}         @{3dsApproved}
EUR_XML_Approved         ${TestMerchant}       EUR         ${XML}          @{3dsApproved}
EUR_FORM_Approved        ${TestMerchant}       EUR         ${FORM}         @{3dsApproved}
RUB_JSON_Approved        ${TestMerchant}       RUB         ${JSON}         @{3dsApproved}
RUB_XML_Approved         ${TestMerchant}       RUB         ${XML}          @{3dsApproved}
RUB_FORM_Approved        ${TestMerchant}       RUB         ${FORM}         @{3dsApproved}
GBP_JSON_Approved        ${TestMerchant}       GBP         ${JSON}         @{3dsApproved}
GBP_XML_Approved         ${TestMerchant}       GBP         ${XML}          @{3dsApproved}
GBP_FORM_Approved        ${TestMerchant}       GBP         ${FORM}         @{3dsApproved}

*** Keywords ***
Server-server full purchase with 3ds card Should Pass
    [Arguments]    ${merchant_id}    ${currency}    ${content_type}    @{credit_card}
    Build required parameters dict    ${merchant_id}    ${currency}    ${specificatons}    ${req_dict_step1}      @{credit_card}
    Send request    ${content_type}    ${url_step1}
    Verify response status    ${specificatons}    ${resp_dict_step1}    ${content_type}
    Save md pareq and acs url for 3ds    ${content_type}
    Build required parameters dict    ${merchant_id}    ${currency}    ${specificatons}    ${req_dict_step2}
    Send request    ${content_type}    ${url_step2}
    Verify response status    ${specificatons}    ${resp_dict_step2}    ${content_type}

Данный человекочитаемый сценарий будет тестировать все 3 поддерживаемые форматы запросов JSON, XML, FORM для 5-ти разных валют: USD, UAH, EUR, RUB, GBP

Запускаем тесты в virtualenv:

(tests) E:workfondyauto_tests>pybot  server-server-tests

Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот - 6

Разработка: автотесты c браузером и Telegram

Теперь добавим файл робота, в котором пропишем все HTML элементы, с которыми мы будем работать: заполнять или анализировать
ui_repository.robot

*** Settings ***
Documentation     Variables used in all tests. Imported one time in resource.txt

*** Variables ***
# Checkout page
${CHECKOUT_BUTTON}                                       css=.btn-lime
${CVV2}                                                  id=cvv2
${EXPIRE_YEAR}                                           id=expire_year
${EXPIRE_MONTH}                                          id=expire_month
${CARD_NUMBER}                                           name=card_number
${CARD_NUMBER_FIELD}                                     id=credit_card_number
${3DS_SUBMIT_BUTTON}                                     xpath=//button[@type='submit']

#Response page
${ORDER_STATUS}                                          css=.field_order_status .value
${TABLE_RESPONSE}                                        id=table_response

в файл resource.robot у нас добавится библиотека Selenium2Library и функция открытия браузера

*** Settings ***
Documentation     A resource file with reusable keywords.

Resource          variables.robot
Resource          cards.robot
Resource          merchants.robot
Resource          ui_repository.robot
Library           Selenium2Library
Library           helper/utils.py
Library           requester.py

*** Keywords ***
Open Browser For Empty Page
    [Arguments]
    Open Browser    about:blank
    Maximize Browser Window

В файл variables.robot добавим название браузера: FireFox

*** Settings ***
Documentation     Variables used in all tests. Imported one time in resource.robot

*** Variables ***
${API SERVER}               api.fondy.eu
${RESP_URL}                 https://${API SERVER}/test/responsepage/
${SERVER}                   fondy.eu
${BROWSER}                  FireFox
${JSON}                     application/json
${XML}                      application/xml
${FORM}                     application/x-www-form-urlencoded

Файл спецификаций теперь пополнился новым набором параметров из документации https://www.fondy.eu/ru/info/api/v1.0/3
Эти спецификации отличаются тем, что реквизиты карты передает не торговец, а они вводятся на стороне платежного шлюза, после редиректа с сайта торговца:

specifications_settings.py

# -*- coding: utf-8 -*-

PURCHASE_FIELDS_REDIRECT = {
    "request": {
        "order_id": {
            "type": "string",
            "required": True,
            "size": 1024
        },
        "merchant_id": {
            "type": "int",
            "required": True,
            "size": 12
        },
        "order_desc": {
            "type": "string",
            "required": True,
            "size": 1024
        },
        "signature": {
            "type": "string",
            "required": True,
            "size": 40
        },
        "amount": {
            "type": "amount",
            "required": True,
            "size": 12
        },
        "currency": {
            "type": "string",
            "required": True,
            "size": 3
        },
        "version": {
            "default": "1.0",
            "type": "string",
            "required": False,
            "size": 10
        },
        "response_url": {
            "type": "url",
            "required": False,
            "size": 2048
        },
        "server_callback_url": {
            "type": "url",
            "required": False,
            "size": 2048
        },
        "payment_systems": {
            "type": "string",
            "required": False,
            "size": 1024
        },
        "default_payment_system": {
            "type": "string",
            "required": False,
            "size": 25
        },
        "lifetime": {
            "default": "36000",
            "type": "int",
            "required": False,
            "size": 6
        },
        "merchant_data": {
            "type": "string",
            "required": False,
            "size": 2048
        },
        "preauth": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "sender_email": {
            "type": "string",
            "required": False,
            "size": 50
        },
        "delayed": {
            "default": True,
            "type": "boolean",
            "required": False
        },
        "lang": {
            "type": "string",
            "required": False,
            "size": 2
        },
        "product_id": {
            "type": "string",
            "required": False,
            "size": 1024
        },
        "required_rectoken": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "verification": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "verification_type": {
            "default": "amount",
            "type": "string",
            "required": False,
            "size": 25
        },
        "rectoken": {
            "type": "string",
            "required": False,
            "size": 40
        },
        "receiver_rectoken": {
            "type": "string",
            "required": False,
            "size": 40
        },
        "design_id": {
            "type": "string",
            "required": False,
            "size": 6
        },
        "subscription": {
            "default": False,
            "type": "boolean",
            "required": False
        },
        "subscription_callback_url": {
            "type": "url",
            "required": False,
            "size": 2048
        }
    },
    "response": PAY_SERVER2SERVER_3DS['response_final'],
}

Детально описывать все файлы тестовых сценариев не буду, в них довольно легко разобраться, приведу только один
pay_with_checkout_url_3ds_approved.robot

pay_with_checkout_url_3ds_approved.robot

*** Settings ***
Documentation     A test suite containing tests related to recurring api transactions with token.
...               Card with 3ds.

Suite Setup       Open Browser For Empty Page
Suite Teardown    Close Browser
Default Tags      3ds    approved
Test Template     Checkout With 3ds Should Pass
Resource          checkout_resources.robot



*** Variables ***
${specificatons}     PURCHASE_FIELDS_REDIRECT
${req_dict_step1}    request
${resp_dict_step1}   response
${url}               https://${API SERVER}/api/checkout/url/
${checkout_url}      ${EMPTY}

***Test Cases***      currency        merchant_id            message        content_type       credit_card
USD_JSON_Approved     USD             ${TestMerchant}       approved        ${JSON}          @{3dsApproved}
USD_XML_Approved      USD             ${TestMerchant}       approved        ${XML}           @{3dsApproved}
USD_FORM_Approved     USD             ${TestMerchant}       approved        ${FORM}          @{3dsApproved}
UAH_JSON_Approved     UAH             ${TestMerchant}       approved        ${JSON}          @{3dsApproved}
UAH_XML_Approved      UAH             ${TestMerchant}       approved        ${XML}           @{3dsApproved}
UAH_FORM_Approved     UAH             ${TestMerchant}       approved        ${FORM}          @{3dsApproved}
EUR_JSON_Approved     EUR             ${TestMerchant}       approved        ${JSON}          @{3dsApproved}
EUR_XML_Approved      EUR             ${TestMerchant}       approved        ${XML}           @{3dsApproved}
EUR_FORM_Approved     EUR             ${TestMerchant}       approved        ${FORM}          @{3dsApproved}
RUB_JSON_Approved     RUB             ${TestMerchant}       approved        ${JSON}          @{3dsApproved}
RUB_XML_Approved      RUB             ${TestMerchant}       approved        ${XML}           @{3dsApproved}
RUB_FORM_Approved     RUB             ${TestMerchant}       approved        ${FORM}          @{3dsApproved}
GBP_JSON_Approved     GBP             ${TestMerchant}       approved        ${JSON}          @{3dsApproved}
GBP_XML_Approved      GBP             ${TestMerchant}       approved        ${XML}           @{3dsApproved}
GBP_FORM_Approved     GBP             ${TestMerchant}       approved        ${FORM}          @{3dsApproved}
*** Keywords ***
Checkout With 3ds Should Pass
    [Arguments]    ${currency}    ${merchant_id}    ${message}    ${content_type}    @{credit_card}
    Get and set checkout url  ${merchant_id}    ${currency}    ${specificatons}    ${req_dict_step1}    ${RESP_URL}    ${content_type}    ${url}    @{credit_card}
    Go to    ${checkout_url}
    Input and submit checkout    ${merchant_id}    @{credit_card}
    Confirm 3ds    ${merchant_id}
    Response page should be displayed
    Check transaction status   ${message}

Для отправки результатов в Telegram нам понадобятся 2 файла: listener и sender

PythonListener.py

from telegram_sender import *


class PythonListener(object):
    ROBOT_LIBRARY_SCOPE = "GLOBAL"
    ROBOT_LISTENER_API_VERSION = 2

    def __init__(self, count=0):
        self.ROBOT_LIBRARY_LISTENER = self
        self.count = count
        self.stat = None

    def end_suite(self, name, attrs):
        self.stat = attrs['statistics']
        return self.stat

    def log_file(self, path):
        print self.stat
        test = Telegram()
        test.telegram_article(self.stat)

telegram_sender.py

import telegram
from helper._settings import *
from telegram.ext import Updater


class Telegram(object):
    def __init__(self, token=None):
        self.token = token or default_token
        self.updater = None
        self.bot = None

    def update_bot(self):
        self.updater = Updater(token=self.token)
        self.bot = telegram.Bot(token=self.token)
        self.updater.start_polling()
        self.bot.getMe()
        self.bot.getUpdates()

    def telegram_article(self, status):
        self.update_bot()
        # chat_id = bot.getUpdates()[-1].message.chat_id  # add this string to update all telegram users
        chat_id = default_user
        self.bot.sendMessage(chat_id=chat_id, text=status)
        self.updater.stop()

Также пропишем в _settings.py параметры для Telegram бота

default_token = None # Put Your bot token to this variable
default_user = None # Add user chat id

как получить токен и id чата можно прочитать например тут и тут:

Теперь собственно запускаем браузерные тесты. Результат должен прийти в телеграм:

(tests) E:workfondyauto_tests>pybot --listener PythonListener.py checkout-tests

Как не дать частым релизам поломать ваше API, или пишем авто-тесты для открытого API и шлем результат в Telegram бот - 7

Послесловие

Надеюсь эта статья будет полезна как автотестеровщикам, так и разработчикам. В следующей статье я постараюсь рассказать о том, что делать если тестов уже несколько тысяч — как их расспараллелить, как собрать метрики о скорости работы тестов, как разработать тесты, взаимодействующие с базой данных

Автор: dayzz

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js