Дратути!
Работая в одной финтех компании 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
<!DOCTYPE html> <html> <head> <title>Run and Result</title> <style> .container { max-width: 800px; margin: 0 auto; } .test-table { width: 100%; border-collapse: collapse; margin: 20px 0; font-family: Arial, sans-serif; } .test-table th { background-color: #f5f5f5; padding: 12px; text-align: left; border-bottom: 2px solid #ddd; } .test-table td { padding: 10px; border-bottom: 1px solid #eee; } .test-pass { background-color: #e8f5e9; color: #2e7d32; } .test-fail { background-color: #ffebee; color: #c62828; } </style> </head> <body> <div class="container" align="center"> <h1>Run and Result</h1> <input type="file" id="configFile" accept=".yaml"> <button onclick="runTests()">Run Tests</button> <div id="results"></div> </div> <script> async function runTests() { const file = document.getElementById('configFile').files[0]; if (!file) return; const formData = new FormData(); formData.append('config', file); try { const response = await fetch('/run-tests', { method: 'POST', body: formData }); const data = await response.json(); displayResults(data.tests); } catch (error) { console.error(error); } } function displayResults(tests) { const resultsDiv = document.getElementById('results'); resultsDiv.innerHTML = ` <table class="test-table"> <thead> <tr> <th>Endpoint</th> <th>Test Name</th> <th>Actual Result</th> <th>Expected Result</th> <th>Result</th> </tr> </thead> <tbody> ${tests.map(test => ` <tr class="${test.passed ? 'test-pass' : 'test-fail'}"> <td>${test.endpoint}</td> <td>${test.type}</td> <td>${test.actual ?? 'N/A'}</td> <td>${test.expected ?? 'N/A'}</td> <td>${test.passed ? '✓ Passed' : '✗ Failed'}</td> </tr> `).join('')} </tbody> </table> `; } </script> </body> </html>
После запуска выглядит это вот так

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

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

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