Import from TestLink

Migrate test cases from TestLink to QA Sphere by exporting XML, converting it to CSV, and importing it

TestLink exports test specifications as XML, and QA Sphere imports test cases from CSV. QA Sphere has no built-in TestLink importer, so a migration has three steps: export XML from TestLink, convert it to a CSV, and import the CSV.

This guide covers TestLink 1.9.x (1.9.20 is the last 1.9 release). The XML layout below was checked against the TestLink source code and its bundled example files in October 2026.

What carries over

TestLinkQA SphereNotes
Test suite hierarchyFolder path, e.g. Regression/LoginFolders are created during import
Test case nameName
Importance 1 / 2 / 3Priority low / medium / high
Summary and PreconditionsPreconditionsSummary first, then preconditions
Steps (actions and expected results)Step N / Expected N
KeywordsTags
External ID (externalid, or fullexternalid when exported with the test case prefix)Legacy IDLets you trace cases back to TestLink
Custom fieldsText custom fieldsOnly if you create the fields in the project first. See the note in Step 2
RequirementsNot importedTestLink exports a requirement ID and title but no URL. QA Sphere's Requirements column needs [Title](URL)
Execution type, version, status, orderNot imported
AttachmentsNot importedTestLink embeds files as base64 in the XML. QA Sphere needs them uploaded through the API first
  1. Open Test Specification and select the test suite to migrate in the tree. To export everything, select the test project root.
  2. Click the Actions (gear) icon to show the operations, then click the Export icon in the Test Suite Operations row. On the project root the icon is Export All Test Suites.
  3. Make sure Export Test Case Summary, Export Test Case Preconditions, Export Test Case Steps, Export Test Case external ID, and Export with keywords are checked. Keywords are unchecked by default. Optionally check With Test Case prefix so the Legacy ID keeps the project prefix. Leave Export attachments unchecked.
  4. Click Export. A suite exports as <suite>.testsuite-deep.xml and a whole project as <project>.testproject-deep.xml.

A suite also has an Export icon in the Test Case Operations row. That one exports only the test cases directly inside the suite, without subsuites or folder structure, as <suite>.testsuite-children-testcases.xml. Use the Test Suite Operations export instead.

A single test case exports as <name>.version<N>.testcase.xml with a <testcases> root, as does the Test Case Operations export above. The script in Step 2 handles both layouts. Test cases from a <testcases> export go into a folder named Imported; rename it after import or edit the Folder column.

The export looks like this. Field values are HTML inside CDATA sections:

<testsuite name="Regression">
  <testsuite name="Login">
    <testcase internalid="25" name="Login with valid credentials">
      <externalid><![CDATA[1]]></externalid>
      <summary><![CDATA[<p>Happy path</p>]]></summary>
      <preconditions><![CDATA[<p>User exists</p>]]></preconditions>
      <importance><![CDATA[3]]></importance>
      <steps>
        <step>
          <step_number><![CDATA[1]]></step_number>
          <actions><![CDATA[<p>Open the login page</p>]]></actions>
          <expectedresults><![CDATA[<p>Form is shown</p>]]></expectedresults>
        </step>
      </steps>
      <keywords><keyword name="smoke"><notes><![CDATA[]]></notes></keyword></keywords>
    </testcase>
  </testsuite>
</testsuite>

Step 2: Convert the XML to a QA Sphere CSV

The script below needs only Python 3 and its standard library. It writes a CSV in QA Sphere's test case format, maps the fields listed in the table above, and converts TestLink's HTML to plain text with Markdown emphasis, list items, and line breaks. Tables, images, and other complex formatting are lost. For rich content, use AI Assisted Import or the API instead.

Save it as testlink_to_qasphere.py:

#!/usr/bin/env python3
"""Convert a TestLink XML export (test suite, test project, or test cases) to a QA Sphere CSV.

Usage: python3 testlink_to_qasphere.py export.xml output.csv
"""
import csv
import html
import re
import sys
import xml.etree.ElementTree as ET

PRIORITY = {"1": "low", "2": "medium", "3": "high"}


def text(elem, *names):
    for name in names:
        child = elem.find(name)
        if child is not None and child.text:
            return child.text.strip()
    return ""


def emphasis(mark):
    def wrap(match):
        inner = match.group(2)
        if not inner.strip():
            return inner
        lead = inner[: len(inner) - len(inner.lstrip())]
        trail = inner[len(inner.rstrip()) :]
        return f"{lead}{mark}{inner.strip()}{mark}{trail}"

    return wrap


def to_markdown(value):
    """Convert TestLink's HTML to plain text with Markdown line breaks."""
    value = re.sub(r"<br\s*/?>", "\n", value, flags=re.I)
    value = re.sub(r"</(p|div|ul|ol|h[1-6])>", "\n\n", value, flags=re.I)
    value = re.sub(r"<li\b[^>]*>\s*", "\n- ", value, flags=re.I)
    value = re.sub(r"<(strong|b)\b[^>]*>(.*?)</\1>", emphasis("**"), value, flags=re.I | re.S)
    value = re.sub(r"<(em|i)\b[^>]*>(.*?)</\1>", emphasis("_"), value, flags=re.I | re.S)
    value = re.sub(r"<[^>]+>", "", value)
    value = "\n".join(line.strip() for line in html.unescape(value).splitlines())
    value = re.sub(r"(?m)^(- .*)\n\n+(?=- )", r"\1\n", value)  # keep lists tight
    return re.sub(r"\n{3,}", "\n\n", value).strip()


def walk(elem, path, rows):
    for suite in elem.findall("testsuite"):
        walk(suite, path + [suite.get("name", "").strip() or "Imported"], rows)
    for case in elem.findall("testcase"):
        steps = []
        for step in case.findall("steps/step"):
            steps.append((to_markdown(text(step, "actions")), to_markdown(text(step, "expectedresults"))))
        tags = [re.sub(r"\s*,\s*", " ", kw.get("name")) for kw in case.findall("keywords/keyword") if kw.get("name")]
        preconditions = to_markdown(text(case, "preconditions"))
        summary = to_markdown(text(case, "summary"))
        if summary:
            preconditions = f"{summary}\n\n{preconditions}".strip()
        rows.append(
            {
                "Folder": "/".join(path) or "Imported",
                "Name": case.get("name", "").strip()[:511],
                "Priority": PRIORITY.get(text(case, "importance"), "medium"),
                "Legacy ID": text(case, "fullexternalid", "externalid")[:255],
                "Tags": ",".join(tags),
                "Preconditions": preconditions,
                "steps": steps,
            }
        )


def main(src, dst):
    root = ET.parse(src).getroot()
    rows = []
    if root.tag == "testsuite":
        name = root.get("name", "").strip()
        walk(root, [name] if name else [], rows)
    else:  # <testcases> root
        walk(root, [], rows)
    max_steps = max((len(r["steps"]) for r in rows), default=0)
    header = ["Folder", "Name", "Priority", "Legacy ID", "Tags", "Preconditions"]
    for i in range(1, max_steps + 1):
        header += [f"Step {i}", f"Expected {i}"]
    with open(dst, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(header)
        for r in rows:
            line = [r[h] for h in header[:6]]
            for step, expected in r["steps"]:
                line += [step, expected]
            line += [""] * (2 * (max_steps - len(r["steps"])))
            writer.writerow(line)
    print(f"Wrote {len(rows)} test cases to {dst}")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Then run it:

python3 testlink_to_qasphere.py export.xml testcases.csv
  • A suite name that contains / is read as two folders. Rename such suites in TestLink before you export.
  • QA Sphere tags are comma-separated, so the script replaces commas in keyword names with spaces.
  • The whole-project export has an unnamed root suite. The script skips it, so the top-level suites become top-level folders.
  • To carry custom fields over, create them in the QA Sphere project first, then add custom_field_text_<systemName> columns to the script output. Values are JSON objects; see Custom Fields.
  • To keep the automation flag, extend the script to add a tag when execution_type is 2.

Step 3: Import the CSV into QA Sphere

  1. Open the project and go to the Test Cases tab
  2. Click three dots
  3. Select Import
  4. Select QA Sphere as the format
  5. Select the comma separator and the folder to import into, then pick testcases.csv

Folders are created from the Folder column, and each row is validated for formatting, field lengths, and required fields. See Importing from the UI for details.

Alternative: AI Assisted Import

If you prefer not to run the script, convert the XML to any flat CSV with one test case per row and one column per field, then choose Other (AI assisted import) in the import dialog. The AI maps the columns, reads HTML in cells, and handles scales such as P1/P2/P3. It costs AI credits and accepts CSV and TSV files up to 1 MB, 5,000 rows, and 256 columns. It does not read XML directly. See AI Assisted Import.

Alternative: API or CLI

For scripted migrations or very large suites, create folders with qasphere api folders bulk-create and test cases with qasphere api test-cases create (see CLI: Public API), or call POST /api/public/v0/project/{project_id}/tcase directly (see Test Cases API). Preconditions and steps accept the supported HTML subset, so you can pass TestLink's HTML through after sanitizing it instead of flattening it to text.

After the import

  • Spot-check a few test cases that have rich text or many steps.
  • Recreate requirement links from the TestLink requirement IDs and titles, either by hand or by adding a Requirements column in [Title](URL) format if your requirements live in a tool with URLs.
  • Upload attachments through the API if you need them.
  • Test runs and execution history are not migrated. Start new runs in QA Sphere.

If your TestLink project is large or uses custom fields heavily, contact our support team and we can help with the migration.