XMind2TestCase converts mind maps into test cases

Background

Quoting the official description

In the software testing process, the most important and core thing is the design of test cases. It is also one of the tasks that the testing team and the testing team spend the most time on daily.
However, the traditional test case design process has many pain points:

Although the cost of using Excel tables for test case design is low, version management is troublesome, maintenance and updates are time-consuming, use case review is cumbersome, and process report statistics are difficult…
Using traditional test management tools such as TestLink, TestCenter, and Redmine, although the execution, management, and statistics of test cases are more convenient, there are still problems such as low efficiency in writing use cases, insufficient divergence of ideas, and time-consuming in the process of rapid product iteration…
The company’s self-developed test management tools are a good choice, but for most small companies and small teams, on the one hand, R&D and maintenance costs are high, and on the other hand, there are certain requirements for technology…

Based on these circumstances, more and more companies are now choosing to use mind mapping, an efficient productivity tool, for use case design, especially agile development teams.

In fact, it has also been proved that the characteristics of mind map’s divergent thinking and graphical thinking are very consistent with the thinking required when designing test cases. Therefore, in actual work, it greatly improves the efficiency of our test case design and is also very convenient. Test case review.

But at the same time, the process of using mind maps for test case design also brings many problems:

It is difficult to quantify the management of test cases, and it is difficult to count the execution status;
It is difficult to connect test case execution results with the BUG management system;
Team members use mind maps to design use cases in different styles, and the communication cost is huge;

Therefore, XMind2TestCase came into being at this time. This tool is based on Python. It formulates a universal template for test cases and then uses XMind, a widely circulated and open source mind mapping tool, to design use cases. Among them, formulating a universal test case template is a very core step (see the usage guide for details). With the universal test case template, we can parse and extract the basic information required for the test case on the XMind file, and then synthesize common The test case import file required by the test case management system. This combines the convenience of XMind for designing test cases with the efficient management of common test case systems!

Currently, XMind2TestCase has implemented test case conversion from XMind files to two common use case management systems, TestLink and Zentao, and also provides two data interfaces after XMind file parsing (Two levels of JSON data, TestSuites and TestCases). It is convenient and quick to connect with other test case management systems.

Example display

Web conversion tool

Use case preview after conversion

XMind2TestCase installation

pip3 install xmind2testcase

upgrade

pip3 install -U xmind2testcase

Mind map use case writing specifications:

Use case example:

XMind2TestCase generates pingcode test case template

Add Excel support

pip install openpyxl

Modify source files

Lib\site-packages\webtool\application.py modify the comments

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
import logging
import os
import re
import arrow
import sqlite3
from contextlib import closing
from os.path import join, exists
from werkzeug.utils import secure_filename
from xmind2testcase.zentao import xmind_to_zentao_csv_file
from xmind2testcase.pingcode import xmind_to_pingcode_excel_file ##Import pingcode method
from xmind2testcase.testlink import xmind_to_testlink_xml_file
from xmind2testcase.utils import get_xmind_testsuites, get_xmind_testcase_list
from flask import Flask, request, send_from_directory, g, render_template, abort, redirect, url_for

here = os.path.abspath(os.path.dirname(__file__))
log_file = os.path.join(here, 'running.log')
# log handler
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s [%(module)s - %(funcName)s]: %(message)s')
file_handler = logging. FileHandler(log_file, encoding='UTF-8')
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG)
stream_handler = logging. StreamHandler()
stream_handler. setFormatter(formatter)
stream_handler.setLevel(logging.INFO)
# xmind to testcase logger
root_logger = logging. getLogger()
root_logger. addHandler(file_handler)
root_logger. addHandler(stream_handler)
root_logger.setLevel(logging.DEBUG)
# flask and werkzeug logger
werkzeug_logger = logging. getLogger('werkzeug')
werkzeug_logger. addHandler(file_handler)
werkzeug_logger. addHandler(stream_handler)
werkzeug_logger.setLevel(logging.DEBUG)

# global variable
UPLOAD_FOLDER = os.path.join(here, 'uploads')
ALLOWED_EXTENSIONS = ['xmind']
DEBUG=True
DATABASE = os.path.join(here, 'data.db3')
HOST = '0.0.0.0'

# flask app
app = Flask(__name__)
app.config.from_object(__name__)
app.secret_key = os.urandom(32)


def connect_db():
    return sqlite3.connect(app.config['DATABASE'])


def init_db():
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
        db. commit()


def init():
    app.logger.info('Start initializing the database...')
    if not exists(UPLOAD_FOLDER):
        os.mkdir(UPLOAD_FOLDER)

    if not exists(DATABASE):
        init_db()
    app.logger.info('Congratulations! the xmind2testcase webtool database has initialized successfully!')


@app.before_request
def before_request():
    g.db = connect_db()


@app.teardown_request
def teardown_request(exception):
    db = getattr(g, 'db', None)
    if db is not None:
        db. close()


def insert_record(xmind_name, note=''):
    c = g.db.cursor()
    now = str(arrow.now())
    sql = "INSERT INTO records (name,create_on,note) VALUES (?,?,?)"
    c.execute(sql, (xmind_name, now, str(note)))
    g. db. commit()


def delete_record(filename, record_id):
    xmind_file = join(app.config['UPLOAD_FOLDER'], filename)
    testlink_file = join(app.config['UPLOAD_FOLDER'], filename[:-5] + 'xml')
    zentao_file = join(app.config['UPLOAD_FOLDER'], filename[:-5] + 'csv')
    pingcode_file = join(app.config['UPLOAD_FOLDER'], filename[:-5] + 'xlsx') # Modify here, add and delete the excel file

    for f in [xmind_file, testlink_file, zentao_file,pingcode_file]: # Modify here, add and delete the excel file
        if exists(f):
            os. remove(f)

    c = g.db.cursor()
    sql = 'UPDATE records SET is_deleted=1 WHERE id = ?'
    c.execute(sql, (record_id,))
    g. db. commit()


def delete_records(keep=20):
    """Clean up files on server and mark the record as deleted"""
    sql = "SELECT * from records where is_deleted<>1 ORDER BY id desc LIMIT -1 offset {}".format(keep)
    assert isinstance(g.db, sqlite3.Connection)
    c = g.db.cursor()
    c. execute(sql)
    rows = c.fetchall()
    for row in rows:
        name = row[1]
        xmind_file = join(app.config['UPLOAD_FOLDER'], name)
        testlink_file = join(app.config['UPLOAD_FOLDER'], name[:-5] + 'xml')
        zentao_file = join(app.config['UPLOAD_FOLDER'], name[:-5] + 'csv')
        pingcode_file = join(app.config['UPLOAD_FOLDER'], name[:-5] + 'xlsx') # Modify here, add and delete the excel file

        for f in [xmind_file, testlink_file, zentao_file,pingcode_file]: # Modify here, add and delete the excel file
            if exists(f):
                os.remove(f)

        sql = 'UPDATE records SET is_deleted=1 WHERE id = ?'
        c.execute(sql, (row[0],))
        g.db.commit()


def get_latest_record():
    found = list(get_records(1))
    if found:
        return found[0]


def get_records(limit=8):
    short_name_length = 120
    c = g.db.cursor()
    sql = "select * from records where is_deleted<>1 order by id desc limit {}".format(int(limit))
    c.execute(sql)
    rows = c.fetchall()

    for row in rows:
        name, short_name, create_on, note, record_id = row[1], row[1], row[2], row[3], row[0]

        # shorten the name for display
        if len(name) > short_name_length:
            short_name = name[:short_name_length] + '...'

        # more readable time format
        create_on = arrow. get(create_on). humanize()
        yield short_name, name, create_on, note, record_id


def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS


def check_file_name(name):
    secured = secure_filename(name)
    if not secured:
        secured = re.sub('[^\w\d] + ', '_', name) # only keep letters and digits from file name
        assert secured, 'Unable to parse file name: {}!'.format(name)
    return secured + '.xmind'


def save_file(file):
    if file and allowed_file(file. filename):
        # filename = check_file_name(file. filename[:-6])
        filename = file.filename
        upload_to = join(app.config['UPLOAD_FOLDER'], filename)

        if exists(upload_to):
            filename = '{}_{}.xmind'.format(filename[:-6], arrow.now().strftime('%Y%m%d_%H%M%S'))
            upload_to = join(app.config['UPLOAD_FOLDER'], filename)

        file.save(upload_to)
        insert_record(filename)
        g.is_success = True
        return filename

    elif file.filename == '':
        g.is_success = False
        g.error = "Please select a file!"

    else:
        g.is_success = False
        g.invalid_files.append(file.filename)


def verify_uploaded_files(files):
    # download the xml directly if only 1 file uploaded
    if len(files) == 1 and getattr(g, 'is_success', False):
        g.download_xml = get_latest_record()[1]

    if g.invalid_files:
        g.error = "Invalid file: {}".format(','.join(g.invalid_files))


@app.route('/', methods=['GET', 'POST'])
def index(download_xml=None):
    g. invalid_files = []
    g.error = None
    g.download_xml = download_xml
    g. filename = None

    if request.method == 'POST':
        if 'file' not in request.files:
            return redirect(request.url)

        file = request.files['file']

        if file.filename == '':
            return redirect(request.url)

        g.filename = save_file(file)
        verify_uploaded_files([file])
        delete_records()

    else:
        g. upload_form = True

    if g. filename:
        return redirect(url_for('preview_file', filename=g.filename))
    else:
        return render_template('index.html', records=list(get_records()))


@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)


@app.route('/<filename>/to/testlink')
def download_testlink_file(filename):
    full_path = join(app.config['UPLOAD_FOLDER'], filename)

    if not exists(full_path):
        abort(404)

    testlink_xmls_file = xmind_to_testlink_xml_file(full_path)
    filename = os.path.basename(testlink_xmls_file) if testlink_xmls_file else abort(404)

    return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)


@app.route('/<filename>/to/zentao')
def download_zentao_file(filename):
    full_path = join(app.config['UPLOAD_FOLDER'], filename)
    if not exists(full_path):
        abort(404)

    zentao_csv_file = xmind_to_zentao_csv_file(full_path)
    filename = os.path.basename(zentao_csv_file) if zentao_csv_file else abort(404)

    return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)

@app.route('/<filename>/to/pingcode') ###New pingcode route
def download_pingcode_file(filename):
    full_path = join(app.config['UPLOAD_FOLDER'], filename)

    if not exists(full_path):
        abort(404)

    pingcode_xlsx_file = xmind_to_pingcode_excel_file(full_path)
    filename = os.path.basename(pingcode_xlsx_file) if pingcode_xlsx_file else abort(404)

    return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)


@app.route('/preview/<filename>')
def preview_file(filename):
    full_path = join(app.config['UPLOAD_FOLDER'], filename)

    if not exists(full_path):
        abort(404)

    testsuites = get_xmind_testsuites(full_path)
    suite_count = 0
    for suite in testsuites:
        suite_count += len(suite.sub_suites)

    testcases = get_xmind_testcase_list(full_path)

    return render_template('preview.html', name=filename, suite=testcases, suite_count=suite_count)


@app.route('/delete/<filename>/<int:record_id>')
def delete_file(filename, record_id):

    full_path = join(app.config['UPLOAD_FOLDER'], filename)
    if not exists(full_path):
        abort(404)
    else:
        delete_record(filename, record_id)
    return redirect('/')


@app.errorhandler(Exception)
def app_error(e):
    return str(e)


def launch(host=HOST, debug=True, port=5001):
    init() # initializing the database
    app.run(host=host, debug=debug, port=port)


if __name__ == '__main__':
    init() # initializing the database
    app.run(HOST, debug=DEBUG, port=5001)

Lib\site-packages\xmind2testcase\parser.py Modify the commented areas

def get_execution_type(topics): ## Annotation content, originally used to define use case types, now changed to remark content
    labels = [topic.get('label', '') for topic in topics]
    # print(labels)
    if None in labels:
        return ''
    else:
        return ' '.join(labels)
    # labels = filter_empty_or_ignore_element(labels)
    #exe_type = 1
    # for item in labels[::-1]:
    # if item.lower() in ['automatic', 'auto', 'automate', 'automation']:
    #exe_type = 2
    #break
    # if item.lower() in ['manual', 'manual', 'manual']:
    #exe_type = 1
    #break
    # return exe_type

Add a new pingcode.py file under \Lib\site-packages\xmind2testcase to define the xlsx format method

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
import csv
import logging
import os
from xmind2testcase.utils import get_xmind_testcase_list, get_absolute_path
import openpyxl
from openpyxl. styles import Font, Color

"""
Convert XMind fie to pingcode testcase csv file

pingcode official document about import CSV testcase file: https://www.pingcode.net/book/pingcodepmshelp/243.mhtml
"""


def xmind_to_pingcode_excel_file(xmind_file):
    """
        Write to excel file xlsx
    """
    xmind_file = get_absolute_path(xmind_file)
    logging.info('Start converting XMind file(%s) to pingcode file...', xmind_file)
    testcases = get_xmind_testcase_list(xmind_file)
    fileheader = ["Module", "Number", "*Title", "Maintainer", "Use case type", "Importance", "Test type", "Estimated work hours", "Associated work items",
                  "Preconditions", "Step description", "Expected results", "Remarks"]
    pingcode_testcase_rows = [fileheader]
    for testcase in testcases:
        row = gen_a_testcase_row(testcase)
        pingcode_testcase_rows.append(row)
    pingcode_file = xmind_file[:-6] + ".xlsx"
    if os.path.exists(pingcode_file):
        os.remove(pingcode_file)
        logging.info('The pingcode csv file already exists, return it directly: %s', pingcode_file)

    workbook = openpyxl.workbook.Workbook()
    ws = workbook.active
    ws.merge_cells('A1:P1') #merge cells
    tips='''
Please follow the rules below to fill in the upload data:\

1. Module: Fill in the name of the existing module in the use case library. Please fill it in completely starting from the first-level module (all use cases do not belong to modules). Use "/" to separate the levels, for example: first-level module/second-level module/ Three-level modules, with a maximum of five levels. If not filled in, it will automatically be classified into 'no module use case'. \

2. Numbering: The numbering style is: test library identification-XX, XX represents a number, such as "QLD-3"; when the number exists under use case management, the use case will be overwritten, and when the number does not exist or the number is not filled in, a new use case will be created. \

3. Title: required and cannot be empty. \

4. Maintainer: Fill in the name or username of the team member. If there are members with the same name in the team, one of them will be randomly selected by default. \

5. Use case type: Optional values: functional testing, performance testing, configuration-related, installation and deployment, interface testing, security-related, compatibility testing, UI testing, and others. \

6. Importance: Optional values: P0, P1, P2, P3, P4. \

7. Test type: optional values: manual, automatic. \

8. Estimated working hours: value. \

9. Associated work items: Fill in the associated requirement number. When filling in multiple values, please separate them with "|". \

10. Precondition: optional. \

11. Step description: text, please fill in the steps with numbers, such as 1.xxx, 2.xxx; fill in groups, add "→" before the sub-steps, such as 1.xxx, →1.xxx; each group or step cell Line break inside. \

12. Expected result: text, keep the number corresponding to the step, such as 1.xxx, 2.xxx; do not fill in the expected result of the group, add "→" before the sub-expected, such as 1. Empty, →1.xxx, each expected Line breaks within the result cell. \

13. Follower: Fill in the name or user name of the team member. If there is a member with the same name in the team, one of the members will be randomly selected by default. When filling in multiple values, please use "|" to separate them. \

14. Remarks: Optional. \

15. Custom attributes use the attribute names created in the system and are not required. \

Tips:\

1. A single import supports up to 5,000 records. \

2. "Title" is a required item. If the required field is empty, it will not be imported. \

    '''
    cell=ws.cell(row=1, column=1)
    cell.value = tips
    cell.font=Font(color=Color(rgb="348FE4")) #Set font color
    # workbook['Sheet'] .row_dimensions[cell.row].height=350 ###Set cell height
    line = 2 #pingcode import reads from the second line
    for data in pingcode_testcase_rows:
        for col in range(1, len(data) + 1):
            ws.cell(row=line, column=col).value = data[col - 1]
        line + = 1
    workbook. save(pingcode_file)
    workbook.close()
    logging.info('Convert XMind file(%s) to a pingcode csv file(%s) successfully!', xmind_file, pingcode_file)
    return pingcode_file



def gen_a_testcase_row(testcase_dict):
    case_module = gen_case_module(testcase_dict['suite'])
    case_title = testcase_dict['name']
    case_precondition = testcase_dict['preconditions']
    case_step, case_expected_result = gen_case_step_and_expected_result(testcase_dict['steps'])
    # case_keyword = ''
    case_priority = gen_case_priority(testcase_dict['importance'])
    case_type = gen_case_type(testcase_dict['execution_type']) ##Remarks
    case_apply_phase = 'Functional Test'
    # row = [case_module, case_title, case_precontion, case_step, case_expected_result, case_keyword, case_priority, case_type, case_apply_phase]
    row = [case_module, "", case_title, "", case_apply_phase, case_priority, 'manual', "", "",
                  case_precontion, case_step, case_expected_result, case_type]
    return row


def gen_case_module(module_name):
    if module_name:
        module_name = module_name.replace('(', '(')
        module_name = module_name. replace(')', ')')
    else:
        module_name = '/'
    return module_name


def gen_case_step_and_expected_result(steps):
    case_step = ''
    case_expected_result = ''

    for step_dict in steps:
        case_step + = str(step_dict['step_number']) + '. ' + step_dict['actions'].replace('\
', '').strip() + '\
'
        case_expected_result + = str(step_dict['step_number']) + '. ' + \
            step_dict['expectedresults'].replace('\
', '').strip() + '\
' \
            if step_dict.get('expectedresults', '') else ''

    return case_step, case_expected_result


def gen_case_priority(priority):
    mapping = {<!-- -->1: 'P0', 2: 'P1', 3: 'P2', 4: 'P3', 5: 'P4'}
    if priority in mapping.keys():
        return mapping[priority]
    else:
        return '中'


def gen_case_type(case_type):
    return case_type
    # mapping = {1: 'manual', 2: 'automatic'}
    # if case_type in mapping. keys():
    # return mapping[case_type]
    #else:
    # return 'manual'


if __name__ == '__main__':
    # xmind_file = '../docs/pingcode_testcase_template.xmind'
    xmind_file = r'C:\Users\
INGMEI\Downloads\xmind2testcase-master\docs\pingcode_testcase_template.xmind'
    pingcode_csv_file = xmind_to_pingcode_excel_file(xmind_file)
    print('Conver the xmind file to a pingcode csv file succssfully: %s', pingcode_csv_file)

Modify index.html

<td><a href="{<!-- -->{ url_for('uploaded_file',filename=record[1]) }}">XMIND</a> |
<a href="{<!-- -->{ url_for('download_zentao_file',filename=record[1]) }}">CSV</a> |
<a href="{<!-- -->{ url_for('download_testlink_file',filename=record[1]) }}">XML</a> |
<a href="{<!-- -->{ url_for('download_pingcode_file', filename=record[1]) }}">EXCEL</a> |{# Modify here! ! ! #}
<a href="{<!-- -->{ url_for('preview_file',filename=record[1]) }}">PREVIEW</a> |
<a href="{<!-- -->{ url_for('delete_file',filename=record[1], record_id=record[4]) }}">DELETE</a>

Modify preview.html

<h2>TestSuites: {<!-- -->{ suite_count }} / TestCases: {<!-- -->{ suite | length }}
/ <a href="{<!-- -->{ url_for("download_zentao_file",filename= name) }}">Get Zentao CSV</a>
/ <a href="{<!-- -->{ url_for("download_testlink_file",filename= name) }}">Get TestLink XML</a>
/ <a href="{<!-- -->{ url_for("download_pingcode_file",filename= name) }}">Get PingCode XLSX</a> {# Modify here! ! ! #}
/ <a href="{<!-- -->{ url_for("index") }}">Go Back</a></h2>

Run

python application.py

Command Line:
xmind2testcase webtool 5001 (port)