CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/testrail-case-management

Author and manage test cases in TestRail via REST API v2 - create cases, organise into suites + sections, update steps + expected results, bulk import from CSV/JSON, set automation status, link to references (Jira / requirements). Covers the Steps / Text / Exploratory templates, custom-field discovery (`get_case_fields`), and pagination on `get_cases`. Use for pre-execution case authoring and repository management. Do NOT use for submitting test-run results (pass/fail, status updates): posting results via add_results_for_cases is a separate post-execution concern.

79

Quality

99%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
testrail-case-management
description:
Author and manage test cases in TestRail via REST API v2 - create cases, organise into suites + sections, update steps + expected results, bulk import from CSV/JSON, set automation status, link to references (Jira / requirements). Covers the Steps / Text / Exploratory templates, custom-field discovery (`get_case_fields`), and pagination on `get_cases`. Use for pre-execution case authoring and repository management. Do NOT use for submitting test-run results (pass/fail, status updates): posting results via add_results_for_cases is a separate post-execution concern.

testrail-case-management

Overview

TestRail organises tests as cases inside sections inside suites inside projects. The API v2 covers full CRUD on each plus templates, custom fields, references, and types. Authentication is HTTP Basic with email + API key per the TestRail support docs (support.testrail.com/hc/en-us/articles/7077871398036-Cases - Cloudflare-protected, cite by stable URL).

For the canonical anatomy this skill operates on, see test-case-anatomy-reference.

Differentiation vs testrail-integration: that skill posts test-run results via add_results_for_cases. This one operates on the case repository - create, update, organise, traceability - a strictly upstream concern.

When to use

  • Creating cases from a spec / requirement / acceptance criterion.
  • Bulk-importing legacy cases from CSV / Excel.
  • Migrating between TestRail instances or to/from another TCM.
  • Programmatic case updates (mass-edit type, priority, tags).
  • Case-repository quality scans.

Authoring

Authentication

Per TestRail API docs (Cloudflare-protected, support.testrail.com):

export TR_BASE="https://your-tenant.testrail.io"
export TR_EMAIL="you@company.com"
export TR_KEY="<api-key-from-user-profile>"
import requests, base64, os, json

auth = base64.b64encode(
    f"{os.environ['TR_EMAIL']}:{os.environ['TR_KEY']}".encode()
).decode()
HEADERS = {
    "Authorization": f"Basic {auth}",
    "Content-Type": "application/json",
}
BASE = os.environ["TR_BASE"]

def api(path, method="GET", body=None):
    url = f"{BASE}/index.php?/api/v2/{path.lstrip('/')}"
    r = requests.request(method, url, headers=HEADERS,
                         data=json.dumps(body) if body else None)
    r.raise_for_status()
    return r.json()

Create a case

POST /index.php?/api/v2/add_case/:section_id:

def create_case(section_id, title, template_id=1, type_id=1, priority_id=2,
                preconditions=None, steps=None, refs=None):
    """
    template_id: 1=Steps, 2=Text, 3=Exploratory
    type_id: per project - discover via get_case_types
    priority_id: 1=Low, 2=Medium, 3=High, 4=Critical (default project enum)
    steps: list of {"content": "Action", "expected": "Outcome"}
    refs: comma-separated requirement IDs (e.g., "REQ-123,REQ-124")
    """
    body = {
        "title": title,
        "template_id": template_id,
        "type_id": type_id,
        "priority_id": priority_id,
    }
    if preconditions:
        body["custom_preconds"] = preconditions
    if steps and template_id == 1:
        body["custom_steps_separated"] = steps
    if refs:
        body["refs"] = refs
    return api(f"add_case/{section_id}", method="POST", body=body)

Steps template (template_id=1) example

steps = [
    {"content": "Navigate to /login", "expected": "Login form rendered"},
    {"content": "Enter alice@example.com + correct password",
     "expected": "Submit button enabled"},
    {"content": "Click Submit",
     "expected": "Redirected to /dashboard within 2 s"},
]
new_case = create_case(
    section_id=42,
    title="Login with valid credentials redirects to dashboard",
    template_id=1,
    steps=steps,
    preconditions="User `alice@example.com` exists; password set to 'pw123'.",
    refs="REQ-AUTH-001",
)
print(new_case["id"])  # e.g., 1234

Discover fields, types + priorities

Each tenant defines its own custom fields, case types, and priorities; discover their IDs at runtime rather than hard-coding them:

fields = api("get_case_fields")   # system_name, label, type_id per field
types = api("get_case_types")     # id + name per case type
prios = api("get_priorities")     # id + name per priority

The type_id enumeration, the field-discovery loop, and the section / suite hierarchy are in references/testrail-api-reference.md.

Update a case

POST /index.php?/api/v2/update_case/:case_id:

def update_case(case_id, **fields):
    return api(f"update_case/{case_id}", method="POST", body=fields)

# Bulk re-tag:
update_case(1234, refs="REQ-AUTH-001,REQ-AUTH-002")

Get + list cases

case = api(f"get_case/1234")
# List with pagination:
cases = []
offset = 0
while True:
    page = api(f"get_cases/{project_id}&suite_id={suite_id}"
               f"&limit=250&offset={offset}")
    cases.extend(page.get("cases", page) if isinstance(page, dict) else page)
    if isinstance(page, dict) and page.get("size", 0) < 250:
        break
    offset += 250

Newer TestRail versions return {"offset", "limit", "size", "_links", "cases": [...]}; older return a bare array. Handle both.

Running

Bulk import from CSV (with per-row duplicate check)

Build name-to-id maps once, then inside the loop check for an existing case by title before create so re-runs stay idempotent. get_cases accepts filter (substring match on title):

import csv

# Name -> id maps, discovered once (see Discover fields, types + priorities)
type_ids = {t["name"]: t["id"] for t in api("get_case_types")}
prio_ids = {p["name"]: p["id"] for p in api("get_priorities")}

created, skipped = [], []
for row in csv.DictReader(open("legacy-cases.csv")):
    title = row["title"]
    existing = api(f"get_cases/{project_id}&suite_id={suite_id}"
                   f"&filter={requests.utils.quote(title[:60])}")
    if existing.get("cases"):
        skipped.append((title, existing["cases"][0]["id"]))
        continue  # already present; skip and log
    steps = [
        {"content": s, "expected": e}
        for s, e in zip(row["steps"].split("|"), row["expected"].split("|"))
    ]
    case = create_case(
        section_id=int(row["section_id"]),
        title=title,
        template_id=1,
        type_id=type_ids[row["type"]],
        priority_id=prio_ids[row["priority"]],
        preconditions=row.get("preconditions"),
        steps=steps,
        refs=row.get("refs"),
    )
    created.append(case["id"])

Verify: assert len(created) + len(skipped) equals the row count before declaring the import done; a shortfall means a row raised - inspect it, fix the source row, and re-run (already-created cases are skipped by the title check).

Parsing results

add_case response is the full case object with id, created_on, updated_on, created_by etc. Permalink:

url = f"{BASE}/index.php?/cases/view/{case_id}"

CI integration

Sync cases from a tests/ directory layout. One pattern: each spec file has front-matter declaring the TestRail case ID; on PR merge, post updates back:

- name: Sync test cases to TestRail
  env:
    TR_BASE: ${{ vars.TR_BASE }}
    TR_EMAIL: ${{ vars.TR_EMAIL }}
    TR_KEY: ${{ secrets.TR_KEY }}
  run: python scripts/sync-testrail.py

Anti-patterns

Anti-patternWhy it failsFix
Hard-coded type_id, priority_idIDs differ per projectDiscover via get_case_types / get_priorities
Steps in custom_steps (Text template)Per-step results unavailableUse custom_steps_separated (Steps template)
Plain title with no refsCoverage reports show 0% requirement coverageAlways set refs to requirement IDs
Creating cases in the root sectionHard to find later; organisation chaosAlways pick a section; create sections as needed
No pagination on get_casesMisses cases beyond first 250Loop with offset until empty
Storing API key in codeToken leakEnvironment variable
Polling for case existence on every CI runRate-limitedCache case-ID-by-title within the CI run

Limitations

  • Cloudflare protection. TestRail support docs require browser validation; this skill cites by stable URL but Headless WebFetch fails. Authenticated API calls work fine (different surface).
  • Custom-field discipline varies. Tenants define different custom fields; scripts must discover field IDs at runtime.
  • refs is free text. TestRail doesn't validate that referenced IDs exist in Jira / Linear / etc. Pair with traceability matrix reconciliation.
  • Hierarchical sections are recursive but display is shallow. Deep section trees are hard to navigate in the UI; keep ≤3 levels.
  • Bulk operations are sequential. No native bulk endpoint; loop + throttle for large imports.

References

  • TestRail API v2 Cases reference - support.testrail.com/hc/en-us/articles/7077871398036-Cases (Cloudflare-protected; cite by stable URL).
  • TestRail API v2 Suites + Sections + Custom Fields docs - support.testrail.com/hc/en-us/categories/7076541806228.
  • Sibling references: test-case-anatomy-reference.
  • Sibling skills (other platforms): xray-case-management, zephyr-scale-case-management, allure-testops-case-management, qase-io-case-management.
  • Sibling-plugin neighbour: testrail-integration (in the qa-test-reporting plugin) - different scope (result sync; not case authoring).

SKILL.md

tile.json