Автоматизация 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

<!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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *