Як не дати частим релізам поламати ваше 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]
# requests for 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 and None 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: invalid signature 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 as Exception 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.

Template Test 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
Template Test 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 users telegram
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 id chat

як отримати токен і id чату можна прочитати наприклад тут, тут:

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

(tests) E:\work\fondy\auto_tests>pybot --listener PythonListener.py checkout-tests



Післямова



Сподіваюся ця стаття буде корисна як автотестеровщикам, так і розробникам. У наступній статті я постараюся розповісти про те, що робити якщо тестів вже кілька тисяч — як їх расспараллелить, як зібрати метрики швидкості роботи тестів, як розробити тести, які взаємодіють з базою даних
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.