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

    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).



    Для простоты исключим Шаг 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.



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

    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, который будет содержать структуру параметров, описанных в документации. Например, параметры в документации



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

    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 '
                                                          'response\n request=%s\n 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:\work\fondy\auto_tests>pybot  server-server-tests
    



    Разработка: автотесты 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:\work\fondy\auto_tests>pybot --listener PythonListener.py checkout-tests
    



    Послесловие


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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 6

      0
      «60 critical tests, 60 passed, 0 failed» — как быстро перестанете обращать внимания на сообщения от сервера?

      И почему Telegram? Просто интересно. Замечаю, что Telgram там, Telegram сям. Почему не старые добрые маилы? Не увеличивает ли это хаос в зоопарке коммуникаций?
        0
        Таки согласен, я бы хотел видеть только упавшие. Потому что это критичная информация.
        0
        В планах добавить боту команды для запуска, перезапуска и остановки тестов, нотификацию на электронную почту мы используем в связке с Sentry для отслеживания критических ошибок.
          0
          Спасибо за статью. Не поясните, отчего тесты выполняются на Windows?
            0
            Тесты изначально разрабатывались под линукс, но во время написания статьи решил перепроверить под Windows, поэтому скриншоты с виндовой командной строкой
              0
              Понятно, а я думал чтобы не возиться с xvfb )
              Кстати, Selenium2Library хорошо дружит с PhantomJS, очень удобно для headless тестирования.

          Only users with full accounts can post comments. Log in, please.