Автоматизация QA без программирования: Как я начал строить No-Code тулзу через конфиги
Дратути!
Работая в одной финтех компании TL QA, я столкнулся с тем, что уровень моих сотрудников по автоматизации не дотягивает до нужного, а рутину хотелось бы автоматизировать. В компании использовался Python (вроде все легко и просто) и все попытки обучить персонал через четкий индивидуальный план развития заканчивались тем, что у сотрудника «не хватало» времени на обучение и поднятие своего грейда как специалиста.
В какой-то момент времени мне пришла в голову идея создания инструмента автоматизации тестирования через конфигурационные файлы. Делать велосипед на подобии Cucumber особо не хотелось, да и сам инструмент в моих фантазиях не подразумевал писать дополнительный код при расширении функционала.
Итак, приступим к интересному.
Первый вариант реализации
В моей голове нарисовался примерный yaml конфиг и я начал думать уже над реализацией «движка» данного инструмента.
tests:
- type: "rest_api"
name: "Test Payment API"
request:
url: "https://api.payment.ru/payment"
method: "GET"
assertions:
- status_code: 200
Так как в компании основной стек был Python, особого выбора ЯП и технологий у меня не было. Стек выбрал для начала Python, Flask.

В файле app.py каких-то интересных моментов, на мой взгляд, нет.
# app.py
from flask import Flask, render_template, request, jsonify
from runner import running_test_config
import uuid
import os
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/run-tests', methods=['POST'])
def run_tests():
if 'config' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['config']
config_path = os.path.join(UPLOAD_FOLDER, f"{uuid.uuid4()}.yaml")
file.save(config_path)
try:
results = running_test_config(config_path)
return jsonify(results)
except Exception as e:
return jsonify({'error': str(e)}), 400
if __name__ == '__main__':
app.run(debug=True)
Больше хотел бы рассказать про runner.py и его изменений в процессе разработки.
В первой его версии получилось вот такое решение, которое чем-то напоминает Cucumber и имеется жесткую привязку к файлу (возможно криво выразился, сейчас покажу)
import yaml
import requests
def run_api_test(test_config):
response = requests.request(
test_config['request']['method'],
test_config['request']['url']
)
results = []
for assertion in test_config['assertions']:
if 'status_code' in assertion:
results.append({
'passed': response.status_code == assertion['status_code'],
'message': f"Expected status {assertion['status_code']}, got {response.status_code}"
})
return results
def running_test_config(config_path):
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
results = []
for test in config.get('tests'):
if test.get('type') == 'rest_api':
results.extend(run_api_test(test))
return {'tests': results}
В методе run_api_test имеется цикл for, в котором все завязывается на статичные значения, которых может и не быть в конфигурационном файле или наоборот блоки в конфиги есть, а проверки в методе нет. Речь про вот этот кусок кода
if 'status_code' in assertion:
results.append({
'passed': response.status_code == assertion['status_code'],
'message': f"Expected status {assertion['status_code']}, got {response.status_code}"
})
Хотелось получать все динамически и не лазить в «движок» инструмента при каких либо добавлениях проверок.
Второй вариант реализации
Опять же начну с новой структуры yaml конфига — на данный момент актуальный. Для примера взял открытый swagger.
tests:
- type: "rest_api"
name: "store/inventory"
request:
url: "https://petstore.swagger.io/v2/store/inventory"
method: "GET"
assertions:
- status_code:
expected_result: 200
actual_result: "{{status}}"
- body:
expected_result: 16
actual_result: "{{body.sold}}"
Основное изменение в данном файле, по сравнению с первым вариантом, это появление actual_result: "{{status}}"
, куда передается значение из ответа запроса.
app.py
остался без изменений, а вот метод run_api_test
из runner.py
пришлось перекроить целиком и полностью, чтобы достичь своей цели (конечно не уверен, что на больших конфигах это сработает гладко и я все предусмотрел). Чтобы не описывать весь код, оставлю комменты.))
def run_api_test(test_config):
# Отправка HTTP-запроса по данным из конфига
response = requests.request(
test_config['request']['method'],
test_config['request']['url']
)
results = []
# Контекст для подстановки значений в шаблоны (статус код и тело ответа())
context = {
'status': response.status_code,
'body': response.json()
}
def change_values(data):
"""Рекурсивно заменяем значения в yaml вида '{{status}}' на реальные значения из контекста."""
if isinstance(data, dict):
# Рекурсивно обходим шаблоны (переменные вида '{{status}}')
return {k: change_values(v) for k, v in data.items()}
if isinstance(data, str) and data.startswith('{{') and data.endswith('}}'):
# Извлекаем пути из шаблона
path = data[2:-2].strip().split('.')
value = context
try:
# Ищем значения по цепочке ключей/индексов
for p in path:
value = value[p] if isinstance(value, dict) else value[int(p)]
return value
except (KeyError, IndexError, TypeError):
return None
# Возвращаем данные как есть, если это не шаблон
return data
# Обработка всех проверок из конфига
for assertion in test_config['assertions']:
# Создаем копию проверок, чтобы не менять исходный конфиг
assertion_copy = deepcopy(assertion)
# Определяем тип проверки (первый ключ в словаре)
assertion_type = list(assertion.keys())[0]
assertion_data = assertion_copy[assertion_type]
# Заменяем шаблоны в данных утверждения на реальные значения
resolved_data = change_values(assertion_data)
# Формируем результат проверки
result = {
'endpoint': test_config['name'],
'type': assertion_type,
'expected': resolved_data['expected_result'],
'actual': resolved_data['actual_result'],
'passed': resolved_data['actual_result'] == resolved_data['expected_result']
}
results.append(result)
return results
На первый взгляд кажется, что все хорошо и можно запускать наши пробные тесты. Делаем это с помощью python3 app.py
Чуть на забыл. У нас же есть еще и index.html
Run and Result
Run and Result
После запуска выглядит это вот так

Необходимо выбрать наш сконфигурированный yaml
файл и нажать Run Tests

В результате работы инструмента мы получаем вот такой результат

В дальнейших планах
1. добавить метод обработки UI тестов, GraphQL тестов
2. добавить генерацию allure report или разработать свой минимальный аналог
Спасибо за внимание.
Если у кого-то возникло желание поконтрибьютить и начать развивать данный проект совместно — велком в репку