Test Plans

Test Case Management in Azure Test Plans

A comprehensive guide to managing test cases in Azure Test Plans, covering test plan creation, test suites, test case authoring, shared steps, configurations, execution tracking, and traceability to requirements and user stories.

Test Case Management in Azure Test Plans

Overview

Azure Test Plans is the test management module in Azure DevOps that organizes manual and automated test cases into plans and suites, tracks execution results, and links everything back to requirements. If you are running a QA process that needs audit trails, traceability to user stories, and structured test execution across multiple configurations, Azure Test Plans is the tool built for it. It is not a replacement for your automated test framework -- it is the layer above it that answers "what did we test, when, and what were the results."

I have used Azure Test Plans on projects ranging from regulated medical device software that requires full test traceability to agile web applications where the QA team runs exploratory sessions alongside automated pipelines. The tool scales to both extremes. This article covers test plan structure, test case authoring, shared steps, configurations, execution tracking, and the REST API scripts I use to automate bulk operations.

Prerequisites

  • An Azure DevOps organization with a project that has Azure Test Plans enabled
  • Basic + Test Plans license assigned to users who will create and manage test plans (readers only need Basic)
  • A backlog with work items (user stories, bugs) for traceability linking
  • Node.js 18+ for the automation scripts
  • Familiarity with Azure Boards work items and Azure DevOps project navigation

Understanding the Test Plan Hierarchy

Azure Test Plans uses a three-level hierarchy:

Test Plan -- The top-level container. Represents a testing effort for a sprint, release, or milestone. Example: "Sprint 24 Regression" or "Release 2.0 Acceptance Testing."

Test Suite -- A grouping within a plan. Suites organize test cases by feature, area, or any logical grouping. Three types exist:

  • Static suites -- manually curated list of test cases
  • Requirement-based suites -- automatically populated from a linked user story or requirement
  • Query-based suites -- dynamically populated from a work item query

Test Case -- The individual test. Contains steps, expected results, and can be linked to automation.

Test Plan: Release 2.0 Acceptance
├── Test Suite: Authentication
│   ├── TC-101: Login with valid credentials
│   ├── TC-102: Login with invalid password
│   └── TC-103: Password reset flow
├── Test Suite: Shopping Cart (requirement-based)
│   ├── TC-201: Add item to cart
│   ├── TC-202: Remove item from cart
│   └── TC-203: Update quantity
└── Test Suite: Regression (query-based)
    └── [Auto-populated from query: all active test cases tagged "regression"]

Creating Test Plans

Through the UI

Navigate to Test Plans > New Test Plan. Provide a name, select an area path and iteration, and optionally set start/end dates. The iteration ties the plan to a sprint, which is useful for sprint-based testing.

Through the REST API

// create-test-plan.js
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var project = process.env.AZURE_DEVOPS_PROJECT || "my-project";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

function apiRequest(method, path, body, callback) {
  var bodyStr = body ? JSON.stringify(body) : null;
  var options = {
    hostname: "dev.azure.com",
    path: "/" + org + "/" + project + "/_apis" + path,
    method: method,
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth
    }
  };
  if (bodyStr) options.headers["Content-Length"] = Buffer.byteLength(bodyStr);

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() { callback(null, res.statusCode, JSON.parse(data)); });
  });
  req.on("error", function(err) { callback(err); });
  if (bodyStr) req.write(bodyStr);
  req.end();
}

var planDefinition = {
  name: "Release 2.0 Acceptance Testing",
  area: { name: project },
  iteration: project + "\\Sprint 24",
  startDate: "2026-02-10T00:00:00Z",
  endDate: "2026-02-24T00:00:00Z"
};

apiRequest("POST", "/testplan/plans?api-version=7.1", planDefinition, function(err, status, data) {
  if (err) return console.error("Error:", err.message);
  if (status === 200 || status === 201) {
    console.log("Test plan created:");
    console.log("  ID: " + data.id);
    console.log("  Name: " + data.name);
    console.log("  Root Suite ID: " + data.rootSuite.id);
  } else {
    console.error("Failed (" + status + "):", JSON.stringify(data));
  }
});

Every test plan automatically gets a root suite. You add child suites under it.

Creating Test Suites

Static Suites

Static suites are manually curated -- you add specific test cases to them:

// create-suite.js
var planId = 12345; // from the test plan creation

var suiteDefinition = {
  suiteType: "staticTestSuite",
  name: "Authentication Tests"
};

apiRequest("POST", "/testplan/plans/" + planId + "/suites?api-version=7.1",
  suiteDefinition, function(err, status, data) {
    if (status === 200 || status === 201) {
      console.log("Suite created: " + data.name + " (ID: " + data.id + ")");
    }
  });

Requirement-Based Suites

Requirement-based suites link to a user story or requirement work item. Test cases associated with that work item automatically appear in the suite:

var suiteDefinition = {
  suiteType: "requirementTestSuite",
  name: "Shopping Cart",
  requirementId: 5678 // Work item ID of the user story
};

apiRequest("POST", "/testplan/plans/" + planId + "/suites?api-version=7.1",
  suiteDefinition, function(err, status, data) {
    if (status === 200 || status === 201) {
      console.log("Requirement suite created: " + data.name);
      console.log("  Linked to work item: " + data.requirementId);
    }
  });

This is the most powerful suite type for traceability. When a product owner asks "did we test the shopping cart feature?" you point them at the requirement-based suite linked to the user story.

Query-Based Suites

Query-based suites dynamically populate from a work item query:

var suiteDefinition = {
  suiteType: "dynamicTestSuite",
  name: "Regression Tests",
  queryString: "SELECT [System.Id] FROM WorkItems WHERE [System.WorkItemType] = 'Test Case' AND [System.Tags] CONTAINS 'regression' AND [System.State] = 'Ready'"
};

apiRequest("POST", "/testplan/plans/" + planId + "/suites?api-version=7.1",
  suiteDefinition, function(err, status, data) {
    if (status === 200 || status === 201) {
      console.log("Query suite created: " + data.name);
    }
  });

Query-based suites update automatically as test cases are tagged or their state changes. I use these extensively for regression suites -- tag a test case with "regression" and it appears in every plan's regression suite.

Authoring Test Cases

Test Case Structure

A test case is a work item of type "Test Case" with structured steps. Each step has an Action (what the tester does) and an Expected Result (what should happen):

// create-test-case.js
var testCase = [
  {
    op: "add",
    path: "/fields/System.Title",
    value: "Login with valid credentials"
  },
  {
    op: "add",
    path: "/fields/System.AreaPath",
    value: project + "\\Authentication"
  },
  {
    op: "add",
    path: "/fields/System.Tags",
    value: "regression; smoke; authentication"
  },
  {
    op: "add",
    path: "/fields/Microsoft.VSTS.TCM.Steps",
    value: '<steps>' +
      '<step id="1" type="ActionStep">' +
        '<parameterizedString isformatted="true">Navigate to the login page at https://app.example.com/login</parameterizedString>' +
        '<parameterizedString isformatted="true">Login page loads with email and password fields visible</parameterizedString>' +
      '</step>' +
      '<step id="2" type="ActionStep">' +
        '<parameterizedString isformatted="true">Enter valid email: @email</parameterizedString>' +
        '<parameterizedString isformatted="true">Email field accepts input</parameterizedString>' +
      '</step>' +
      '<step id="3" type="ActionStep">' +
        '<parameterizedString isformatted="true">Enter valid password: @password</parameterizedString>' +
        '<parameterizedString isformatted="true">Password field shows masked input</parameterizedString>' +
      '</step>' +
      '<step id="4" type="ActionStep">' +
        '<parameterizedString isformatted="true">Click the "Sign In" button</parameterizedString>' +
        '<parameterizedString isformatted="true">User is redirected to the dashboard. Welcome message displays the user name.</parameterizedString>' +
      '</step>' +
    '</steps>'
  },
  {
    op: "add",
    path: "/fields/Microsoft.VSTS.TCM.Parameters",
    value: '<parameters>' +
      '<param name="email">[email protected]</param>' +
      '<param name="password">SecureP@ss123</param>' +
    '</parameters>'
  }
];

var options = {
  hostname: "dev.azure.com",
  path: "/" + org + "/" + project + "/_apis/wit/workitems/$Test%20Case?api-version=7.1",
  method: "POST",
  headers: {
    "Content-Type": "application/json-patch+json",
    "Authorization": "Basic " + auth
  }
};

var bodyStr = JSON.stringify(testCase);
options.headers["Content-Length"] = Buffer.byteLength(bodyStr);

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    var result = JSON.parse(data);
    if (res.statusCode === 200) {
      console.log("Test case created: #" + result.id + " - " + result.fields["System.Title"]);
    } else {
      console.error("Failed:", JSON.stringify(result));
    }
  });
});

req.write(bodyStr);
req.end();

Parameterized Test Cases

The @email and @password placeholders in the steps above are parameters. You can define multiple sets of parameter values, and the test case runs once per set during execution. This is useful for data-driven testing:

@email @password Expected
[email protected] SecureP@ss123 Success
[email protected] AdminP@ss456 Success
[email protected] wrong Failure message shown

Parameters are defined in the Microsoft.VSTS.TCM.Parameters field as XML, and the parameter data (iterations) are stored in Microsoft.VSTS.TCM.LocalDataSource.

Shared Steps

When multiple test cases share common setup or verification steps, use Shared Steps to avoid duplication. A Shared Step is a separate work item that can be referenced from any test case.

Creating Shared Steps

// create-shared-steps.js
var sharedStep = [
  {
    op: "add",
    path: "/fields/System.Title",
    value: "Login as standard user"
  },
  {
    op: "add",
    path: "/fields/Microsoft.VSTS.TCM.Steps",
    value: '<steps>' +
      '<step id="1" type="ActionStep">' +
        '<parameterizedString isformatted="true">Navigate to https://app.example.com/login</parameterizedString>' +
        '<parameterizedString isformatted="true">Login page loads</parameterizedString>' +
      '</step>' +
      '<step id="2" type="ActionStep">' +
        '<parameterizedString isformatted="true">Enter email: [email protected] and password: SecureP@ss123</parameterizedString>' +
        '<parameterizedString isformatted="true">Credentials accepted</parameterizedString>' +
      '</step>' +
      '<step id="3" type="ActionStep">' +
        '<parameterizedString isformatted="true">Click "Sign In"</parameterizedString>' +
        '<parameterizedString isformatted="true">Dashboard loads with welcome message</parameterizedString>' +
      '</step>' +
    '</steps>'
  }
];

var options = {
  hostname: "dev.azure.com",
  path: "/" + org + "/" + project + "/_apis/wit/workitems/$Shared%20Steps?api-version=7.1",
  method: "POST",
  headers: {
    "Content-Type": "application/json-patch+json",
    "Authorization": "Basic " + auth
  }
};

var bodyStr = JSON.stringify(sharedStep);
options.headers["Content-Length"] = Buffer.byteLength(bodyStr);

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    var result = JSON.parse(data);
    if (res.statusCode === 200) {
      console.log("Shared steps created: #" + result.id + " - " + result.fields["System.Title"]);
      console.log("Reference this in test cases as a shared step.");
    }
  });
});

req.write(bodyStr);
req.end();

Then reference the shared step in test cases by inserting a SharedStepReference element in the steps XML. When you update the shared step, every test case that references it picks up the change automatically. This is invaluable for login flows, setup procedures, and common verification steps.

Test Configurations

Configurations define the environments or conditions under which test cases execute. Common configurations include browser type, operating system, device, and locale.

Creating Configurations

// create-configuration.js
var configValues = [
  { name: "Chrome - Windows", values: [
    { name: "Browser", value: "Chrome 120" },
    { name: "OS", value: "Windows 11" }
  ]},
  { name: "Firefox - Windows", values: [
    { name: "Browser", value: "Firefox 121" },
    { name: "OS", value: "Windows 11" }
  ]},
  { name: "Safari - macOS", values: [
    { name: "Browser", value: "Safari 17" },
    { name: "OS", value: "macOS Sonoma" }
  ]},
  { name: "Chrome - Android", values: [
    { name: "Browser", value: "Chrome Mobile" },
    { name: "OS", value: "Android 14" }
  ]}
];

configValues.forEach(function(config) {
  var body = {
    name: config.name,
    description: config.values.map(function(v) { return v.name + ": " + v.value; }).join(", "),
    values: config.values
  };

  apiRequest("POST", "/testplan/configurations?api-version=7.1", body, function(err, status, data) {
    if (status === 200 || status === 201) {
      console.log("Configuration created: " + data.name + " (ID: " + data.id + ")");
    }
  });
});

When you assign configurations to a test suite, Azure Test Plans creates one test point per test case per configuration. If a suite has 10 test cases and 4 configurations, you get 40 test points to execute.

Test Execution and Tracking

Running Tests

Test execution happens in the Azure Test Plans web runner or the Test & Feedback browser extension. For each test point, the tester:

  1. Opens the test runner
  2. Steps through each action
  3. Marks each step as Passed or Failed
  4. Attaches screenshots, comments, or bug references for failures
  5. Saves the result

Querying Test Results via API

// get-test-results.js
var planId = process.argv[2];
var suiteId = process.argv[3];

if (!planId || !suiteId) {
  console.error("Usage: node get-test-results.js <planId> <suiteId>");
  process.exit(1);
}

var path = "/testplan/plans/" + planId + "/suites/" + suiteId + "/testpoint?api-version=7.1";

apiRequest("GET", path, null, function(err, status, data) {
  if (err) return console.error("Error:", err.message);

  var points = data.value || [];
  var summary = { passed: 0, failed: 0, blocked: 0, notRun: 0 };

  console.log("Test Points for Suite " + suiteId + " in Plan " + planId + ":");
  console.log("=========================================================");

  points.forEach(function(point) {
    var testCase = point.testCaseReference;
    var config = point.configuration;
    var result = point.results ? point.results.outcome : "Not Run";

    if (result === "Passed") summary.passed++;
    else if (result === "Failed") summary.failed++;
    else if (result === "Blocked") summary.blocked++;
    else summary.notRun++;

    console.log("  " + testCase.name);
    console.log("    Config: " + config.name);
    console.log("    Outcome: " + result);
    console.log("");
  });

  console.log("Summary:");
  console.log("  Passed: " + summary.passed);
  console.log("  Failed: " + summary.failed);
  console.log("  Blocked: " + summary.blocked);
  console.log("  Not Run: " + summary.notRun);
  console.log("  Total: " + points.length);

  var passRate = points.length > 0 ?
    (summary.passed / points.length * 100).toFixed(1) : 0;
  console.log("  Pass Rate: " + passRate + "%");
});

Traceability: Linking Tests to Requirements

The real power of Azure Test Plans is traceability -- linking test cases to user stories, bugs, and requirements to answer "is this feature tested?" and "what test failed that caused this bug?"

Linking Test Cases to Work Items

// link-test-to-story.js
var testCaseId = process.argv[2];
var userStoryId = process.argv[3];

if (!testCaseId || !userStoryId) {
  console.error("Usage: node link-test-to-story.js <testCaseId> <userStoryId>");
  process.exit(1);
}

var linkPatch = [
  {
    op: "add",
    path: "/relations/-",
    value: {
      rel: "Microsoft.VSTS.Common.TestedBy-Reverse",
      url: "https://dev.azure.com/" + org + "/" + project + "/_apis/wit/workitems/" + userStoryId,
      attributes: { comment: "Test coverage for user story #" + userStoryId }
    }
  }
];

var options = {
  hostname: "dev.azure.com",
  path: "/" + org + "/" + project + "/_apis/wit/workitems/" + testCaseId + "?api-version=7.1",
  method: "PATCH",
  headers: {
    "Content-Type": "application/json-patch+json",
    "Authorization": "Basic " + auth
  }
};

var bodyStr = JSON.stringify(linkPatch);
options.headers["Content-Length"] = Buffer.byteLength(bodyStr);

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode === 200) {
      console.log("Linked test case #" + testCaseId + " to user story #" + userStoryId);
    } else {
      console.error("Failed (" + res.statusCode + "):", data);
    }
  });
});

req.write(bodyStr);
req.end();

Once linked, the user story's "Tests" tab shows all associated test cases and their latest results. This is the traceability audit trail that compliance teams require.

Complete Working Example

This script creates a complete test plan with suites, test cases, shared steps, and configurations for a login feature:

// setup-test-plan.js -- Complete test plan scaffold
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var project = process.env.AZURE_DEVOPS_PROJECT || "my-project";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

function apiCall(method, path, body, contentType, callback) {
  var bodyStr = body ? JSON.stringify(body) : null;
  var options = {
    hostname: "dev.azure.com",
    path: "/" + org + "/" + project + "/_apis" + path,
    method: method,
    headers: {
      "Content-Type": contentType || "application/json",
      "Authorization": "Basic " + auth
    }
  };
  if (bodyStr) options.headers["Content-Length"] = Buffer.byteLength(bodyStr);

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() {
      var parsed;
      try { parsed = JSON.parse(data); } catch (e) { parsed = data; }
      callback(null, res.statusCode, parsed);
    });
  });
  req.on("error", function(err) { callback(err); });
  if (bodyStr) req.write(bodyStr);
  req.end();
}

function createTestCase(title, steps, tags, callback) {
  var stepsXml = '<steps>' + steps.map(function(s, i) {
    return '<step id="' + (i + 1) + '" type="ActionStep">' +
      '<parameterizedString isformatted="true">' + s.action + '</parameterizedString>' +
      '<parameterizedString isformatted="true">' + s.expected + '</parameterizedString>' +
    '</step>';
  }).join("") + '</steps>';

  var body = [
    { op: "add", path: "/fields/System.Title", value: title },
    { op: "add", path: "/fields/Microsoft.VSTS.TCM.Steps", value: stepsXml },
    { op: "add", path: "/fields/System.Tags", value: tags },
    { op: "add", path: "/fields/System.State", value: "Ready" }
  ];

  apiCall("POST", "/wit/workitems/$Test%20Case?api-version=7.1", body,
    "application/json-patch+json", function(err, status, data) {
      if (err) return callback(err);
      if (status === 200) {
        console.log("  Created test case: #" + data.id + " - " + title);
        callback(null, data.id);
      } else {
        callback(new Error("Failed to create test case: " + title));
      }
    });
}

// Step 1: Create the test plan
console.log("Creating test plan...");
var plan = {
  name: "Sprint 24 - Authentication Testing",
  area: { name: project },
  startDate: new Date().toISOString(),
  endDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString()
};

apiCall("POST", "/testplan/plans?api-version=7.1", plan, null, function(err, status, planData) {
  if (err) return console.error("Error:", err.message);
  console.log("Plan created: #" + planData.id + " - " + planData.name);
  console.log("");

  // Step 2: Create a static suite
  var suite = { suiteType: "staticTestSuite", name: "Login Tests" };
  apiCall("POST", "/testplan/plans/" + planData.id + "/suites?api-version=7.1", suite, null,
    function(err, status, suiteData) {
      console.log("Suite created: #" + suiteData.id + " - " + suiteData.name);
      console.log("");

      // Step 3: Create test cases
      var testCases = [
        {
          title: "Login with valid credentials",
          tags: "regression; smoke; authentication",
          steps: [
            { action: "Navigate to /login", expected: "Login page displays" },
            { action: "Enter valid email and password", expected: "Fields accept input" },
            { action: "Click Sign In", expected: "Dashboard loads with welcome message" }
          ]
        },
        {
          title: "Login with invalid password",
          tags: "regression; authentication; negative",
          steps: [
            { action: "Navigate to /login", expected: "Login page displays" },
            { action: "Enter valid email and invalid password", expected: "Fields accept input" },
            { action: "Click Sign In", expected: "Error message: Invalid email or password" },
            { action: "Verify no redirect occurs", expected: "Still on login page" }
          ]
        },
        {
          title: "Login with locked account",
          tags: "regression; authentication; negative",
          steps: [
            { action: "Attempt login with locked account credentials", expected: "Login page displays" },
            { action: "Click Sign In", expected: "Error: Account locked. Contact support." },
            { action: "Verify lockout timer displays", expected: "Message shows remaining lockout time" }
          ]
        },
        {
          title: "Password reset flow",
          tags: "regression; authentication",
          steps: [
            { action: "Click 'Forgot Password' link on login page", expected: "Password reset page loads" },
            { action: "Enter registered email address", expected: "Email field accepts input" },
            { action: "Click 'Send Reset Link'", expected: "Success message: Check your email" },
            { action: "Open reset email and click link", expected: "Password reset form loads" },
            { action: "Enter new password meeting requirements", expected: "Password accepted" },
            { action: "Confirm new password", expected: "Passwords match" },
            { action: "Click 'Reset Password'", expected: "Success message, redirect to login" }
          ]
        }
      ];

      console.log("Creating test cases...");
      var index = 0;
      var createdIds = [];

      function createNext() {
        if (index >= testCases.length) {
          console.log("");
          console.log("Setup complete!");
          console.log("  Plan: #" + planData.id);
          console.log("  Suite: #" + suiteData.id);
          console.log("  Test Cases: " + createdIds.join(", "));
          return;
        }

        var tc = testCases[index++];
        createTestCase(tc.title, tc.steps, tc.tags, function(err, id) {
          if (!err) createdIds.push(id);
          setTimeout(createNext, 500);
        });
      }
      createNext();
    });
});
node setup-test-plan.js

# Output:
# Creating test plan...
# Plan created: #101 - Sprint 24 - Authentication Testing
#
# Suite created: #201 - Login Tests
#
# Creating test cases...
#   Created test case: #301 - Login with valid credentials
#   Created test case: #302 - Login with invalid password
#   Created test case: #303 - Login with locked account
#   Created test case: #304 - Password reset flow
#
# Setup complete!
#   Plan: #101
#   Suite: #201
#   Test Cases: 301, 302, 303, 304

Common Issues and Troubleshooting

1. Test Plans Option Not Available in Navigation

Error: The "Test Plans" option does not appear in the Azure DevOps sidebar.

Azure Test Plans requires the Basic + Test Plans access level. Users with only the Basic license can view test results but cannot create or manage test plans. An organization administrator must assign the correct access level under Organization Settings > Users.

2. Test Cases Not Appearing in Requirement Suite

Error: You linked a test case to a user story, but the requirement-based suite is empty.

The link type must be "Tests / Tested By." Other link types (Related, Parent/Child) do not populate requirement-based suites. Verify the link by opening the user story and checking the "Tests" tab. Also ensure the test case's area path matches the suite's area path filter.

3. Shared Steps Not Updating in Test Cases

Error: You edited a shared step, but test cases still show the old version.

The web runner caches shared steps. Close and reopen the test runner to pick up changes. In the UI, the test case editor should show updated steps immediately -- if it does not, the shared step reference may be broken. Re-add the shared step reference to the test case.

4. Configuration Matrix Creates Too Many Test Points

Error: Assigning 5 configurations to a suite with 50 test cases creates 250 test points, which is overwhelming.

Not every test case needs to run on every configuration. Assign configurations at the test case level instead of the suite level. Critical test cases run on all configurations; less critical ones run on the primary configuration only.

5. Cannot Delete Test Cases That Have Results

Error: Attempting to delete a test case returns an error about existing test results.

Test cases with execution history cannot be deleted -- they can only be set to Closed state. This is by design to preserve the audit trail. If you need to remove them from a suite, remove the suite assignment without deleting the work item.

6. Bulk Import of Test Cases Fails

Error: Importing test cases from CSV or Excel fails with format errors.

The Steps field requires specific XML formatting. When importing, provide steps as plain text with numbered lines -- Azure DevOps converts them to XML on import. Use the official Azure DevOps Excel plugin for the most reliable bulk import experience.

Best Practices

  1. Use requirement-based suites for feature testing. Link every test case to its parent user story. This gives you automatic traceability and answers "what percentage of this feature is tested?" without manual tracking.

  2. Use query-based suites for regression testing. Tag test cases with "regression" and create a query-based suite that collects them automatically. New regression tests are included immediately without manual suite management.

  3. Keep test case steps atomic and verifiable. Each step should describe one action and one expected result. Avoid steps like "complete the checkout process" -- break it into individual verification points.

  4. Use shared steps for common procedures. Login flows, navigation setup, and data preparation should be shared steps. When the login process changes, you update one shared step instead of fifty test cases.

  5. Assign configurations strategically, not universally. Not every test case needs to run on every browser and OS combination. Designate a core set of test cases for full configuration coverage and run the rest on the primary configuration.

  6. Tag test cases for discoverability. Use consistent tags like "smoke," "regression," "critical," and feature names. Tags power query-based suites and make it easy to find test cases across the project.

  7. Close, do not delete, obsolete test cases. Deleting test cases destroys execution history. Move obsolete test cases to a "Closed" state and remove them from active suites. The history remains available for audits.

  8. Review test plan coverage before each sprint. At sprint planning, verify that new user stories have corresponding test cases or that existing test cases cover the changes. Gaps in coverage should be addressed before development starts.

  9. Automate test plan creation for recurring releases. Use the REST API scripts in this article to scaffold test plans, suites, and configurations for each release. Manual setup is error-prone and time-consuming.

  10. Link failed test cases to bugs immediately. When a test case fails, create a bug from the test runner. This automatically links the bug to the test case and the requirement, providing a complete defect trail.

References

Powered by Contentful