![image](https://habrastorage.org/getpro/habr/post_images/3a1/3a6/6af/3a13a66afc5b9153c20cecedd24ff299.jpg)
Предисловие
Наша команда разрабатывает финансовые инструменты, в том числе открытые платежные 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](https://habrastorage.org/getpro/habr/post_images/c15/43b/610/c1543b6108a98ee46f7ac8e32e40daf4.png)
Для начала разработки установим
- virtualenv
pip install virtualenv setuptools
- бесплатную версию PyCharm Edu
https://www.jetbrains.com/pycharm-edu/download/ - для 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 оказалась не совместимой - клонируем проект с github — этот пункт можно пропустить, если есть желание проделать все с нуля:
git clone https://github.com/cloudipsp/auto_tests.git
- устанавливаем зависимости:
для начала нам достаточно таких библиотек:
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).
![](https://habrastorage.org/files/031/22a/843/03122a843de840a1be57181ae5dc961e.png)
Для простоты исключим Шаг 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.
![](https://habrastorage.org/files/368/457/3c9/3684573c90f945d68189ee948c591223.png)
Теперь создадим файлы робота:
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, который будет содержать структуру параметров, описанных в документации. Например, параметры в документации
![](https://habrastorage.org/files/d9e/522/a58/d9e522a588654c98a32caaf6a1252d8c.png)
будут соответствовать структуре:
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
![](https://habrastorage.org/files/bbc/749/d8f/bbc749d8fe1f4559a5fdb4ea6099f6f9.png)
Разработка: автотесты 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
![](https://habrastorage.org/files/ddb/7bb/46c/ddb7bb46c0214545a99901541f771d19.png)
Послесловие
Надеюсь, эта статья будет полезна как автотестеровщикам, так и разработчикам. В следующей статье я постараюсь рассказать о том, что делать если тестов уже несколько тысяч — как их распараллелить, как собрать метрики о скорости работы тестов, как разработать тесты, взаимодействующие с базой данных.