Зачем нам это, почему бы сразу не написать чек лист?

В начале немного о себе, мое основное занятие - обеспечение качества на вверенных проектах, я Senior QA в комп��нии Umbrella IT. В работе часто приходится сталкиваться с необходимостью составления такой тестовой документации как тест кейсы и чек листы. Кому как больше нравится, но то, что на это занятие тратится часть времени, которое можно было бы использовать на что-то более прикладное или творческое, меня несколько печалит. 

Подмечу, что для “конвертирования” творческой мысли в ветвящуюся последовательность логических действий я давно использую такой инструмент, как mind map. В контексте тестирования он позволяет не только быстро отразить иерархию логики проверок, которая прямо сейчас у меня “на уме”, но и через визуальное подкрепление наводит на новые идеи, - чем можно эти проверки расширить, что добавить и как оптимизировать. К сожалению, на сколько mind map прекрасно подходит для формирования вариаций проверок, на столько же он и неудобен для исполнения этих проверок. А каждый раз вручную копипастить деревья из mind map в чек листы используемой на проекте Test Management System (далее TMS) конечно можно, но это совершенно не творческое и жутко затратное по времени занятие. Отсюда и возникает необходимость наличия инструмента для автоматической конвертации mind map в чек листы

Инструкция к применению

Делаем mind map

В выборе инструмента формирования mind map будем отталкиваться от того, что можно получить бесплатно. Да, можно срезать углы, используя платные планы Miro + Mind map downloader или XMInd + встроенный экспорт в Excel. Но, во-первых это не так интересно, а во-вторых не факт, что вам будет удобно использовать формат экспорта, который предоставляют эти инструменты “из коробки”. Мой вы��ор пал на XMInd (бесплатный план) так как он поддерживает локальную установку на разные платформы, работу из облака + формат его сейвов подходит для конвертации open source инструментами.

Сформируем какую-нибудь простую карту исключительно для демонстрации и сохраним ее. Для дальнейших операций сразу обозначим на каком уровне иерархии карты у нас будут папки/секции/разделы, на каком кейсы/чек листы, а на каком шаги. Заранее примем тот факт, что последний уровень иерархии - это вариации/условия внутри шага тест кейса/чек листа.

Тут сразу подсвечу, что для дальнейшего успешного маппинга в иерархию TMS необходимо расставить метки иерархии уровней в именах наших топиков/узлов. На нашей карте я расставил следующие флаги:

  • “Level.”, “Level2.” и “Level3.” - флаги уровней иерархии подразделов

  • “Case.” - флаг, который показывает, где начинается выделенный тест кейс/чек лист 

  • “Step.” - флаг, который показывает выделенный шаг тест кейса/чек листа

Примет mind map
Примет mind map

Переводим mind map в JSON

Как промежуточный формат для конвертации, который удобно передать в обработку чему угодно (нейросети, скрипту, API и тд), будем использовать JSON. Тут заведомо можно сказать, что сохраненный файл с расширением xmind это ZIP архив, в котором есть файл content.json, а уже это JSON представление вашей карты. Впрочем в нем достаточно много лишних для нас полей,  для получения чего-то более удобного и лаконичного воспользуемся свободным конвертером xmindparser. Установить и использовать его можно различными способами, в этой статье будем пользоваться заранее установленным pip (документация по установке pip). Наши действия:

  1. Устанавливаем xmindparser командой: pip install xmindparser .

  2. Переходим в директорию с установленным xmindparser. Тут можно чуть больше потрудиться и добавить путь к исполняемому файлу в список команд консоли ОС, но статья не об этом, поэтому идем простым и быстрым путем. Команда для получения информации об установленном пакете: pip show xmindparser

  3. Конвертируем файл xmind с заранее сохраненной mind map в формат JSON командой: xmindparser ВАШ_ФАЙЛ.xmind -json

Пример выполнения команды в терминале
Пример выполнения команды в терминале

На этом этапе мы уже получаем удобоваримый и понятный JSON файл, отражающий иерархию наших проверок. Как раз его мы и будем использовать для конвертации в чек листы.

Полученный JSON файл
Полученный JSON файл

Делаем из JSON чек листы

Самое простое, что можно использовать это конвертировать полученный JSON через доступную вам нейросеть в чек листы формата офисных приложений (редактиру��мые документы или таблицы). Это конечно самое простое, ну а мы попробуем пойти дальше, - с помощью локальных приложений на Python конвертировать полученный JSON в что-то с возможностью импортирования в TMS. В качестве TMS выберем Testops и Testrail. Заранее подсвечу, что в этой статье мы будем создавай чек листы, не смотря на то, что в этих TMS создаются тест кейсы. Причиной тому - цель этой статьи, а именно показать быстрый и простой способ сократить затраты на работу с тестовой документацией.

Testops

К сожалению Testops не умеет импортировать тест кейсы из файлов с JSON форматом, поэтому будем конвертировать в CSV. Сам Testops довольно гибко настраивается и от проекта к проекту у него может быть довольно разнообразный набор обязательных полей с различными именами. Предположим, что в нашем примере у кейсов Testops есть обязательные поля:

  • “name” - имя тест кейса/чек листа

  • “scenario” - сам сценарий, с шагами

    • Внутри scenario шаги начинаются с новой строки с флагом “[step *]”, например [step 1] и [step 2]   

  • “Section1” - верхний раздел, уровень иерархии

  • “Section2” - вложенный раздел, уровень иерархии

Соответственно наша CSV таблица должна иметь примерно такой формат
Соответственно наша CSV таблица должна иметь примерно такой формат

Создадим папку для этого приложения, назовем ее xmind_to_csv. Внутри этой папки разместим файл с именем mindmap.json - это наш ранее полученный JSON.  Там же создадим файл xmind_json_to_csv.pyв который сохраним “навайбкоденное” приложение-конвертер на Python.

Код приложения-конвертера из JSON в CSV для импорта чек листов в Testops
import json
import csv
import re

# Загружаем JSON
with open("mindmap.json", encoding="utf-8") as f:
    data = json.load(f)

rows = []
all_section_columns = set()  # для динамических Section колонок

def process_case(case, section_levels):
    """Обрабатывает Case, формирует сценарий и добавляет строку"""
    name = case["title"].replace("Case. ", "")
    scenario_lines = []

    if "topics" in case:
        for step_index, step in enumerate(case["topics"], start=1):
            if step["title"].startswith("Step."):
                step_name = step["title"].replace("Step. ", "")
                scenario_lines.append(f"[step {step_index}] {step_name}")
                if "topics" in step:
                    for i, t in enumerate(step["topics"], start=1):
                        scenario_lines.append(f"{i}. {t['title']}")
                scenario_lines.append("")  # пустая строка между блоками шагов

    scenario = "\n".join(scenario_lines).strip()

    # Формируем строку с динамическими Section колонками
    row = {"name": name, "scenario": scenario}
    for i, sec in enumerate(section_levels, start=1):
        col_name = f"Section{i}"
        row[col_name] = sec
        all_section_columns.add(col_name)

    rows.append(row)

def traverse(node, section_levels=None):
    if section_levels is None:
        section_levels = []

    title = node.get("title", "")

    # Если верхний уровень функционала (не Case и не Step), добавляем как Section1, если ещё пусто
    if not section_levels:
        section_levels = [title]

    # Проверяем, является ли уровень Level, Level2, Level3 и т.д.
    match = re.match(r"(Leve?l\d*\.?)\s*(.+)", title, re.IGNORECASE)
    if match:
        # Добавляем уровень в конец списка
        section_levels = section_levels + [match.group(2).strip()]

    # Если Case, обрабатываем
    if title.startswith("Case."):
        process_case(node, section_levels)

    # Рекурсивный обход детей
    for child in node.get("topics", []):
        traverse(child, section_levels)

# Запускаем обход
for item in data:
    traverse(item["topic"])

# Определяем все колонки
section_columns_sorted = sorted(all_section_columns, key=lambda x: int(x.replace("Section", "")))
fieldnames = ["name", "scenario"] + section_columns_sorted

# Записываем CSV
with open("test_cases.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(rows)

print("CSV успешно создано!")

Запускаем приложение командой python xmind_json_to_csv.py и получаем в нашей папке файл test_cases.csv, который можем успешно импортировать в Testops

Подсвечу, что процесс импорта может сильно отличаться от проекта к проекту. Тут я покажу пример, как это может быть:

  1. Внутри вашей группы или подпроекта жмем Import

    скриншот
  2. На открывшейся странице кликаем по кнопке Submit и вторым действием  выбираем CSV файл который мы ранее создали, после чего опять жмем кнопку Submit

    скриншот
  3. На следующей странице мы можем настроить вручную особенности маппинга нашей таблицы (наличие заголовков, разделители и тд), но в этой статье ограничимся настройками по умолчанию. Жмем на кнопку Parse file

    скриншот
  4. Далее мы определяем какая колонка CSV файла будет мапиться в какое поле наших чек листов. После распределения всех колонок по нужным нам полям можно прожать кнопку Show preview и в левой панели посмотреть как будут выглядеть будущие чек листы. Если результат, увиденный в preview вас устраивает, - нажимайте на кнопку Submit import.

    скриншот
  5. В зависимости от настроек проекта вас могут попросить выбрать статусы создаваемых кейсов. Выбираем и жмем Submit.

  6. Дожидаемся завершения обработки.

    скриншот
  7. Проверяем импортированные кейсы. В зависимости от ваших потребностей вы можете расширять список полей, которые получаете из mind map. Эта статья призвана показать сам механизм/возможность подобной автоматизации. Остальное может быть настроено под ваши локальные потребности.

    Успешный результат импорта чек листов в Testops
    Успешный результат импорта чек листов в Testops

Testrail

И опять таки к еще одному сожалению Testrail тоже не умеет импортировать тест кейсы из файлов с JSON форматом. Попробуем расширить возможности нашего конвертера. Так как выше предложенная библиотека xmindparser может быть импортирована сразу в наше приложение, я решил сразу привести наше локальное приложение к виду полноценного CLI приложения, в котором на вход мы сможем подавать как файлы JSON, ранее полученные с помощью xmindparser, так и сразу файлы с расширением “.xmind” с возможностью указания в какой формат CSV мы хотим его сконвертировать, для Testops или Testrail.


Для Testrail маппинг будет несколько проще, так как “из коробки” экспорт и импорт тест кейсов не поддерживает иерархию Секций/Подсекций. Необходимая иерархия будет добавлена в имя самого чек листа. Если у вас есть желание и возможность внедрить поддержку Секций/Подсекций Testrail в импорт - вы можете на все усмотрение, изменив код приложения, под нужды своего проекта. Код приложения будет выглядеть следующим образом:

Код CLI приложения для конвертации xmind и JSON в CSV для импорта чек листов в Testops и Testrail
import json
import csv
import re
import sys
import os
import logging

# Импортируем парсер XMind
try:
    from xmindparser import xmind_to_dict, config as xmind_config
except ImportError:
    print("❗ Модуль 'xmindparser' не установлен. Установите его командой:")
    print("   pip install xmindparser")
    sys.exit(1)


# ---------- Настройки парсера XMind ----------
xmind_config.update({
    'logName': 'xmind_parser',
    'logLevel': logging.ERROR,
    'logFormat': '%(asctime)s %(levelname)-8s: %(message)s',
    'showTopicId': False,
    'hideEmptyValue': True
})


# ---------- Вспомогательная функция ----------
def load_input_file(input_path):
    """Загружает JSON или XMind, возвращает структуру данных (dict/list)."""
    if not os.path.isfile(input_path):
        print(f"❗ Ошибка: файл '{input_path}' не найден.")
        sys.exit(1)

    ext = os.path.splitext(input_path)[1].lower()

    if ext == ".json":
        with open(input_path, encoding="utf-8") as f:
            data = json.load(f)
        return data

    elif ext == ".xmind":
        print(f"📘 Обнаружен XMind-файл, выполняется парсинг: {input_path}")
        data = xmind_to_dict(input_path)
        return data

    else:
        print("❗ Поддерживаются только файлы .json или .xmind")
        sys.exit(1)


# ---------- TESTOPS ----------
def convert_json_to_csv_testops(data, output_path):
    """Преобразует структуру JSON MindMap в CSV по логике TestOps"""
    rows = []
    all_section_columns = set()

    def process_case(case, section_levels):
        name = case["title"].replace("Case. ", "")
        scenario_lines = []

        if "topics" in case:
            for step_index, step in enumerate(case["topics"], start=1):
                if step["title"].startswith("Step."):
                    step_name = step["title"].replace("Step. ", "")
                    scenario_lines.append(f"[step {step_index}] {step_name}")
                    if "topics" in step:
                        for i, t in enumerate(step["topics"], start=1):
                            scenario_lines.append(f"{i}. {t['title']}")
                    scenario_lines.append("")

        scenario = "\n".join(scenario_lines).strip()
        row = {"name": name, "scenario": scenario}

        for i, sec in enumerate(section_levels, start=1):
            col_name = f"Section{i}"
            row[col_name] = sec
            all_section_columns.add(col_name)

        rows.append(row)

    def traverse(node, section_levels=None):
        if section_levels is None:
            section_levels = []

        title = node.get("title", "")

        if not section_levels:
            section_levels = [title]

        match = re.match(r"(Leve?l\d*\.?)\s*(.+)", title, re.IGNORECASE)
        if match:
            section_levels = section_levels + [match.group(2).strip()]

        if title.startswith("Case."):
            process_case(node, section_levels)

        for child in node.get("topics", []):
            traverse(child, section_levels)

    for item in data:
        traverse(item["topic"])

    section_columns_sorted = sorted(all_section_columns, key=lambda x: int(x.replace("Section", "")))
    fieldnames = ["name", "scenario"] + section_columns_sorted

    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)

    print(f"✅ CSV (TestOps) успешно создано: {output_path}")


# ---------- TESTRAIL ----------
def convert_json_to_csv_testrail(data, output_path):
    """Преобразует структуру JSON MindMap в CSV по логике TestRail"""
    rows = []

    def process_case(case, level_titles):
        case_name = case["title"].replace("Case. ", "").strip()
        full_title_parts = [case_name] + [lvl for lvl in level_titles if lvl]
        full_title = ". ".join(full_title_parts)

        steps = []
        if "topics" in case:
            for step in case["topics"]:
                if step["title"].startswith("Step."):
                    step_name = step["title"].replace("Step. ", "").strip()
                    substeps = []
                    if "topics" in step:
                        for i, t in enumerate(step["topics"], start=1):
                            substeps.append(f"{i}. {t['title']}")
                    step_text = step_name
                    if substeps:
                        step_text += "\n" + "\n".join(substeps)
                    steps.append(step_text)

        for idx, step_text in enumerate(steps):
            rows.append({
                "Title": full_title if idx == 0 else "",
                "Steps (Step)": step_text
            })

    def traverse(node, current_levels=None):
        if current_levels is None:
            current_levels = []

        title = node.get("title", "")
        match = re.match(r"(Leve?l\d*\.?)\s*(.+)", title, re.IGNORECASE)
        local_levels = current_levels.copy()

        if match:
            local_levels.append(match.group(2).strip())

        if title.startswith("Case."):
            process_case(node, local_levels)

        for child in node.get("topics", []):
            traverse(child, local_levels)

    for item in data:
        traverse(item["topic"])

    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["Title", "Steps (Step)"])
        writer.writeheader()
        writer.writerows(rows)

    print(f"✅ CSV (TestRail) успешно создано: {output_path}")


# ---------- MAIN ----------
def main():
    if len(sys.argv) < 3:
        print("❗ Использование:")
        print("   python json_to_csv.py путь_к_json_или_xmind_файлу -testops")
        print("   python json_to_csv.py путь_к_json_или_xmind_файлу -testrail")
        sys.exit(1)

    input_path = sys.argv[1]
    mode = sys.argv[2].lower()

    data = load_input_file(input_path)

    base_name = os.path.splitext(os.path.basename(input_path))[0]
    dir_name = os.path.dirname(input_path)

    if mode == "-testops":
        output_path = os.path.join(dir_name, f"{base_name}_testops.csv")
        convert_json_to_csv_testops(data, output_path)
    elif mode == "-testrail":
        output_path = os.path.join(dir_name, f"{base_name}_testrail.csv")
        convert_json_to_csv_testrail(data, output_path)
    else:
        print("⚙️  Неизвестный режим. Доступные ключи:")
        print("   -testops   — экспорт для TestOps")
        print("   -testrail  — экспорт для TestRail")


if __name__ == "__main__":
    main()

Так как наше приложение прибавило в функционале, немного переименуем исполняемый файл, теперь это xmind_to_csv.py.

Запускаем скрипт командой python xmind_json_to_csv.py file_name -key , например python xmind_json_to_csv.py auth_regtest.xmind -testrail или python xmind_json_to_csv.py auth_regtest_xmind -testops и получаем наш CSV файл (в нашем случае при конвертации файла auth_regtest.xmind получаем auth_regtest_testrail.csv) . Импортируем наши чек листы в Testrail:

  1. Переходим в ��ужную Секцию нашего проекта кликаем по кнопке Импорта сущностей и выбираем импорт из CSV

    скриншот

    Далее в открывшемся окне:

    1. выбираем импортируемый файл

    2. настраиваем опции чтения CSV (как правило менять опции не нужно, чтение успешно с настройками по умолчанию)

    3. жмем по кнопке Next

      скриншот
  2. На следующем шаге:

    1. выбираем опцию использования нескольких строк для одного чек листа

    2. выбираем разделителем для чек листов колонку Tille

    3. настраиваем маппинг колонок таблицы на поля тест кейса/чек листа

    4. жмем по кнопке перехода на следующий шаг

      скриншот
  3. Следующий шаг пропускаем, этот маппинг выходит за скоуп этой статьи, жмем Next

  4. На последнем шаге нас встречает превью наших будущих чек листов, если все устраивает - жмем Import

    скриншот

Если импорт был успешен, нам предложат скачать файл конфигурации маппинга (пригодится, в последующих импортах, чтобы не настраивать маппинг каждый раз заново), жмем на кнопку Close. Наши чек листы успешно импортированы, можно пользоваться.

Успешный результат импорта чек листов в Testrail
Успешный результат импорта чек листов в Testrail

Заключение

В заключении еще раз подсвечу, предложенное решение не позволяет наполнить создаваемые сущности полями с предусловиями и ожидаемыми результатами для генерируемых шагов, что является необходимым для создания полноценных тест кейсов. Данная статья призвана показать краткий и простой путь для конвертирования mind map в чек листы вышеуказанных TMS, что позволяет удобно создавать вариации различных условий и состояний для планируемого процесса тестирования и быстро конвертировать эти вариации в удобный чек лист для непосредственного исполнения самих проверок.

Версии использованных приложений и библиотек на момент написания статьи:

  • Xmind версия 25.07

  • Xmindparser версия 1.0.9

  • Python версия 3.14