Test Plans

Parameterized Tests and Shared Steps

Create data-driven test cases with parameters and reusable shared steps in Azure Test Plans with REST API automation

Parameterized Tests and Shared Steps

Parameterized tests and shared steps are two features in Azure Test Plans that dramatically reduce test maintenance overhead while increasing coverage. Parameters let you run the same test logic against multiple data sets without duplicating test cases, and shared steps let you define common procedures once and reference them across dozens of test cases. Together, they form the backbone of a scalable manual and automated testing strategy in Azure DevOps.

Prerequisites

Before working through this article, you should have the following in place:

  • An Azure DevOps organization and project with Azure Test Plans enabled (Basic + Test Plans license)
  • A Personal Access Token (PAT) with Work Items (Read & Write) and Test Management (Read & Write) scopes
  • Node.js v16 or later installed locally
  • Familiarity with Azure DevOps work item types and the REST API basics
  • Basic understanding of test case structure (steps, expected results, test suites)

What Are Parameterized Tests?

A parameterized test case in Azure Test Plans is a single test case whose steps contain parameter placeholders. When you execute the test, Azure DevOps generates one iteration for each row in the parameter data table. Instead of creating five separate test cases for five different user roles, you create one test case with a @role parameter and five rows of data.

This is the same concept as data-driven testing in automated frameworks like Jest's test.each or NUnit's TestCase attribute, but applied to manual test execution inside Azure Test Plans. The tester sees each iteration as a separate pass through the steps, with the parameter values substituted inline.

The benefits are straightforward. You maintain one set of steps instead of many. When the workflow changes, you update one test case instead of hunting down every duplicate. Your test suite stays lean, and your data coverage stays broad.

Parameter Syntax and Data Tables

Parameters in Azure Test Plans use the @parameterName syntax directly inside test step text. When you type @username in a step action or expected result, Azure DevOps recognizes it as a parameter and adds it to the test case's parameter data table.

Here is what a parameterized test case looks like in practice:

Step 1: Navigate to the login page and enter @username in the username field.

Step 2: Enter @password in the password field and click Sign In.

Step 3: Verify the dashboard displays the greeting "Welcome, @displayName".

The parameter data table for this test case might look like:

@username @password @displayName
[email protected] P@ssw0rd1 Admin User
[email protected] ViewPass1 Read Only User
[email protected] EditPass1 Content Editor

When a tester runs this test case, they execute three iterations. Each iteration substitutes the values from one row into the steps. The tester marks each iteration as passed or failed independently.

Parameter Naming Rules

Parameter names must start with a letter and can contain letters, numbers, and underscores. They are case-insensitive, so @UserName and @username reference the same parameter. Avoid spaces and special characters. Keep names descriptive but short — @expectedStatusCode is better than @esc, but @theExpectedHTTPStatusCodeReturnedByTheAPI is too much.

Creating Data-Driven Test Cases

To create a parameterized test case in the Azure DevOps web interface:

  1. Open Test Plans and navigate to your test suite.
  2. Create a new test case or open an existing one.
  3. In the Steps tab, type your step actions using @parameterName wherever you want variable data.
  4. Azure DevOps automatically detects the parameters and adds columns to the Parameter Values grid below the steps.
  5. Add rows to the grid for each data combination you want to test.
  6. Save the test case.

Each row in the parameter grid becomes one iteration during test execution. The tester can see all iterations in the Test Runner and step through them sequentially or skip to specific ones.

Shared Steps: Reusable Test Procedures

Shared steps solve a different but equally painful problem. When twenty test cases all start with "Log in as an admin user," you end up maintaining that login procedure in twenty places. When the login flow changes — say, two-factor authentication gets added — you update twenty test cases. Shared steps let you define the login procedure once and reference it from all twenty test cases.

A shared step is a special work item type in Azure DevOps (Microsoft.VSTS.TCM.SharedSteps). It contains a set of ordered steps, just like a test case, but it exists independently. Test cases reference shared steps by inserting them as a single "step" that expands at execution time.

Creating Shared Steps

You can create shared steps in two ways.

From scratch: In the Test Plans section, create a new Shared Steps work item. Add your steps with actions and expected results, then save.

From existing test case steps: Open a test case, select the steps you want to extract, and click Create shared steps. Azure DevOps creates a new shared step work item and replaces the selected steps in the test case with a reference to it. This is the most common approach because you typically discover the need for shared steps after you have already written several test cases with duplicated procedures.

Shared Step Structure

A shared step work item contains:

  • Title: A descriptive name like "Login as administrator" or "Navigate to payment checkout"
  • Steps: Ordered actions and expected results, identical in format to test case steps
  • Parameters: Shared steps can contain @parameter placeholders, which we will cover shortly
  • Attachments: Screenshots, documents, or files relevant to the procedure
  • History: Full version history, just like any other work item

Linking Shared Steps to Test Cases

To insert a shared step into a test case:

  1. Open the test case in the editor.
  2. Click on the step row where you want to insert the shared step (it will be inserted above the selected step).
  3. Click the Insert shared steps button in the toolbar.
  4. Search for and select the shared step work item.
  5. The shared step appears as a collapsible block within the test case steps.

During test execution, the shared step expands into its individual steps. The tester sees them inline, marked with a visual indicator showing they come from a shared step.

A test case can reference multiple shared steps, and a shared step can be referenced by any number of test cases. This many-to-many relationship is the real power of the feature.

Parameterized Shared Steps

This is where things get interesting. Shared steps can contain parameters, and those parameters integrate with the test case's parameter data table. When a test case references a shared step that uses @username, the @username parameter appears in the test case's data grid alongside any parameters defined in the test case's own steps.

Consider a shared step called "Authenticate User" with these steps:

Step 1: Navigate to @loginUrl and enter @username.

Step 2: Enter @password and click Login.

Step 3: Verify the user dashboard loads.

Now a test case references this shared step and adds its own steps:

[Shared] Authenticate User (expands to 3 steps above)

Step 4: Navigate to @targetPage.

Step 5: Verify @expectedElement is visible on the page.

The combined parameter table becomes:

@loginUrl @username @password @targetPage @expectedElement
/login [email protected] Pass1 /settings User Management Panel
/login [email protected] Pass2 /dashboard Read-Only Dashboard

The parameters from the shared step and the test case merge into a single data table. Each iteration supplies values for all parameters regardless of where they are defined.

Iteration-Based Test Execution

When a tester runs a parameterized test case, the Test Runner presents iterations sequentially. Each iteration is a complete pass through all steps with one row of parameter data. The execution model works like this:

  1. The Test Runner loads the first iteration and substitutes parameter values into all steps.
  2. The tester executes each step, marking it as Passed, Failed, or Blocked.
  3. After completing all steps, the tester moves to the next iteration.
  4. Each iteration has an independent outcome — iteration 1 can pass while iteration 3 fails.
  5. The overall test case result reflects the worst outcome across all iterations. If any iteration fails, the test case is marked as failed.

This iteration model maps directly to data-driven test execution in automated frameworks, making it natural to transition between manual and automated testing.

You can also associate bugs with specific iterations. When iteration 2 fails because the viewer role sees admin controls, you file a bug linked to that specific iteration, preserving the exact parameter values that caused the failure.

Data-Driven Automated Tests with Jest

If you are automating your test cases, the parameterized pattern translates directly to Jest's test.each. Here is a practical example that mirrors a parameterized Azure Test Plans test case:

var axios = require("axios");

var testData = [
  { username: "[email protected]", password: "P@ssw0rd1", expectedRole: "Administrator" },
  { username: "[email protected]", password: "ViewPass1", expectedRole: "Viewer" },
  { username: "[email protected]", password: "EditPass1", expectedRole: "Editor" }
];

describe("User Authentication and Role Verification", function() {
  test.each(testData)(
    "should authenticate $username and verify role is $expectedRole",
    function(data) {
      return axios.post("https://api.example.com/auth/login", {
        username: data.username,
        password: data.password
      })
      .then(function(response) {
        expect(response.status).toBe(200);
        expect(response.data.role).toBe(data.expectedRole);
      });
    }
  );
});

When you run automated tests linked to Azure Test Plans test cases, the test results map back to iterations. Each element in the testData array corresponds to one iteration in the test case. This alignment is critical for traceability — you can trace a failed automated iteration back to the exact parameter row in the test plan.

Combining Parameters with Configurations

Azure Test Plans also supports test configurations — environment combinations like browser type, operating system, or API version. When you combine parameterized test cases with configurations, you get a full matrix.

A test case with 3 parameter rows assigned to 2 configurations (Chrome, Firefox) produces 6 test points: 3 iterations times 2 configurations. Each test point is independently executable and trackable.

This matrix approach is powerful but can explode quickly. Three parameters with five rows each, assigned to four configurations, produces twenty test points. Be deliberate about which configurations you assign to parameterized test cases. Not every data row needs to run on every configuration.

Bulk Parameter Management via REST API

For large-scale test suites, managing parameter data through the web UI becomes tedious. The Azure DevOps REST API lets you programmatically create, read, and update parameterized test cases and their data tables.

Test case parameters are stored in the work item's Microsoft.VSTS.TCM.LocalDataSource field as an XML string. The XML structure contains a table of parameter names and values. Here is the format:

<NewDataSet>
  <xs:schema id="NewDataSet" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="NewDataSet">
      <xs:complexType>
        <xs:choice maxOccurs="unbounded">
          <xs:element name="Table1">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="username" type="xs:string" minOccurs="0" />
                <xs:element name="password" type="xs:string" minOccurs="0" />
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <Table1>
    <username>[email protected]</username>
    <password>P@ssw0rd1</password>
  </Table1>
  <Table1>
    <username>[email protected]</username>
    <password>ViewPass1</password>
  </Table1>
</NewDataSet>

To update parameters via API, you PATCH the work item and set this XML field. The steps themselves are stored in Microsoft.VSTS.TCM.Steps, which uses a separate XML format for step actions and expected results containing @parameterName references.

Shared Step Versioning

Shared steps are work items, so they carry full revision history. Every edit creates a new revision. However, test cases that reference a shared step always use the latest version — there is no version pinning.

This means updating a shared step immediately affects all test cases that reference it. That is usually what you want, but it can cause problems if you update a shared step mid-sprint while testers are actively executing tests. The steps they see will change between executions.

Best practice is to update shared steps between test cycles, not during them. If you need to make a breaking change, communicate it to your test team before saving.

You can query the revision history of a shared step through the REST API to understand when changes were made and by whom:

var axios = require("axios");

var org = "my-org";
var project = "my-project";
var sharedStepId = 4521;
var pat = process.env.AZURE_DEVOPS_PAT;

var url = "https://dev.azure.com/" + org + "/" + project +
  "/_apis/wit/workitems/" + sharedStepId + "/revisions?api-version=7.1";

axios.get(url, {
  auth: { username: "", password: pat }
})
.then(function(response) {
  var revisions = response.data.value;
  revisions.forEach(function(rev) {
    console.log("Rev " + rev.rev + " by " +
      rev.fields["System.ChangedBy"].displayName +
      " on " + rev.fields["System.ChangedDate"]);
  });
})
.catch(function(err) {
  console.error("Failed to fetch revisions:", err.message);
});

Converting Test Steps to Shared Steps

When you realize that multiple test cases contain identical step sequences, you can extract those steps into a shared step and replace the duplicates. The web UI supports this for individual test cases, but for bulk conversion, you need the REST API.

The process involves:

  1. Identify the shared steps XML in the source test case's Microsoft.VSTS.TCM.Steps field.
  2. Create a new Shared Steps work item with those steps.
  3. Update each test case that contains the duplicated steps, replacing them with a shared step reference.

The shared step reference in the test case steps XML uses a <compref> element:

<compref id="2" ref="4521" />

Where ref is the work item ID of the shared step. This replaces the individual <step> elements that were extracted.

Complete Working Example

Here is a comprehensive Node.js script that creates parameterized test cases with shared steps, populates data tables, and generates a test execution matrix via the Azure DevOps REST API.

var axios = require("axios");

// Configuration
var config = {
  org: process.env.AZURE_ORG || "my-organization",
  project: process.env.AZURE_PROJECT || "my-project",
  pat: process.env.AZURE_DEVOPS_PAT,
  apiVersion: "7.1",
  testPlanId: parseInt(process.env.TEST_PLAN_ID, 10) || 100,
  testSuiteId: parseInt(process.env.TEST_SUITE_ID, 10) || 200
};

var baseUrl = "https://dev.azure.com/" + config.org + "/" + config.project;
var authHeader = {
  auth: { username: "", password: config.pat },
  headers: { "Content-Type": "application/json-patch+json" }
};

// Build XML parameter data source from array of objects
function buildParameterXml(params, rows) {
  var schema = params.map(function(p) {
    return '              <xs:element name="' + p + '" type="xs:string" minOccurs="0" />';
  }).join("\n");

  var dataRows = rows.map(function(row) {
    var fields = params.map(function(p) {
      return "    <" + p + ">" + escapeXml(row[p] || "") + "</" + p + ">";
    }).join("\n");
    return "  <Table1>\n" + fields + "\n  </Table1>";
  }).join("\n");

  var xml = '<NewDataSet>\n' +
    '  <xs:schema id="NewDataSet" xmlns:xs="http://www.w3.org/2001/XMLSchema">\n' +
    '    <xs:element name="NewDataSet">\n' +
    '      <xs:complexType>\n' +
    '        <xs:choice maxOccurs="unbounded">\n' +
    '          <xs:element name="Table1">\n' +
    '            <xs:complexType>\n' +
    '              <xs:sequence>\n' +
    schema + "\n" +
    '              </xs:sequence>\n' +
    '            </xs:complexType>\n' +
    '          </xs:element>\n' +
    '        </xs:choice>\n' +
    '      </xs:complexType>\n' +
    '    </xs:element>\n' +
    '  </xs:schema>\n' +
    dataRows + "\n" +
    '</NewDataSet>';

  return xml;
}

// Build XML steps with parameter references
function buildStepsXml(steps, sharedStepRefs) {
  var stepId = 1;
  var xml = '<steps id="0" last="' + (steps.length + sharedStepRefs.length) + '">\n';

  sharedStepRefs.forEach(function(ref) {
    xml += '  <compref id="' + stepId + '" ref="' + ref + '" />\n';
    stepId++;
  });

  steps.forEach(function(step) {
    xml += '  <step id="' + stepId + '" type="ValidateStep">\n';
    xml += '    <parameterizedString isformatted="true">' +
      escapeXml(step.action) + '</parameterizedString>\n';
    xml += '    <parameterizedString isformatted="true">' +
      escapeXml(step.expected || "") + '</parameterizedString>\n';
    xml += '  </step>\n';
    stepId++;
  });

  xml += '</steps>';
  return xml;
}

function escapeXml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

// Create a work item (test case or shared step)
function createWorkItem(type, fields) {
  var url = baseUrl + "/_apis/wit/workitems/$" + type + "?api-version=" + config.apiVersion;
  var body = fields.map(function(f) {
    return {
      op: "add",
      path: "/fields/" + f.field,
      value: f.value
    };
  });

  return axios.post(url, body, authHeader)
    .then(function(response) {
      console.log("Created " + type + ": #" + response.data.id + " - " +
        response.data.fields["System.Title"]);
      return response.data;
    });
}

// Add test case to a test suite
function addToTestSuite(testCaseId) {
  var url = baseUrl + "/_apis/test/Plans/" + config.testPlanId +
    "/Suites/" + config.testSuiteId + "/TestCase/" + testCaseId +
    "?api-version=" + config.apiVersion;

  return axios.post(url, null, {
    auth: authHeader.auth,
    headers: { "Content-Type": "application/json" }
  });
}

// Get test points for a suite (the execution matrix)
function getTestPoints() {
  var url = baseUrl + "/_apis/test/Plans/" + config.testPlanId +
    "/Suites/" + config.testSuiteId +
    "/points?api-version=" + config.apiVersion;

  return axios.get(url, { auth: authHeader.auth })
    .then(function(response) {
      return response.data.value;
    });
}

// Main execution flow
function main() {
  if (!config.pat) {
    console.error("Error: AZURE_DEVOPS_PAT environment variable is required");
    process.exit(1);
  }

  var sharedStepId;

  // Step 1: Create a shared step for the login procedure
  console.log("\n--- Creating Shared Step ---");
  var loginSteps = buildStepsXml([
    {
      action: "Navigate to @loginUrl",
      expected: "Login page loads with username and password fields"
    },
    {
      action: "Enter @username in the username field and @password in the password field",
      expected: "Credentials are entered"
    },
    {
      action: "Click the Sign In button",
      expected: "User is authenticated and redirected to @landingPage"
    }
  ], []);

  createWorkItem("Microsoft.VSTS.TCM.SharedSteps", [
    { field: "System.Title", value: "Authenticate User - Shared Login Procedure" },
    { field: "Microsoft.VSTS.TCM.Steps", value: loginSteps }
  ])
  .then(function(sharedStep) {
    sharedStepId = sharedStep.id;
    console.log("Shared step created with ID: " + sharedStepId);

    // Step 2: Create a parameterized test case referencing the shared step
    console.log("\n--- Creating Parameterized Test Case ---");

    var testSteps = buildStepsXml([
      {
        action: "Navigate to the @targetSection section",
        expected: "@targetSection page loads successfully"
      },
      {
        action: "Verify the page title contains @expectedTitle",
        expected: "Page title reads: @expectedTitle"
      },
      {
        action: "Verify the user's role badge shows @expectedRole",
        expected: "Role badge displays @expectedRole"
      },
      {
        action: "Attempt to access the @restrictedAction feature",
        expected: "@accessResult"
      }
    ], [sharedStepId]);

    var parameterNames = [
      "loginUrl", "username", "password", "landingPage",
      "targetSection", "expectedTitle", "expectedRole",
      "restrictedAction", "accessResult"
    ];

    var parameterRows = [
      {
        loginUrl: "https://app.example.com/login",
        username: "[email protected]",
        password: "AdminP@ss1",
        landingPage: "/admin/dashboard",
        targetSection: "User Management",
        expectedTitle: "User Management Console",
        expectedRole: "Administrator",
        restrictedAction: "Delete User",
        accessResult: "Action is permitted and confirmation dialog appears"
      },
      {
        loginUrl: "https://app.example.com/login",
        username: "[email protected]",
        password: "EditorP@ss1",
        landingPage: "/editor/dashboard",
        targetSection: "Content Library",
        expectedTitle: "Content Library",
        expectedRole: "Editor",
        restrictedAction: "Delete User",
        accessResult: "Action is blocked with 403 Forbidden message"
      },
      {
        loginUrl: "https://app.example.com/login",
        username: "[email protected]",
        password: "ViewerP@ss1",
        landingPage: "/viewer/dashboard",
        targetSection: "Reports",
        expectedTitle: "Analytics Reports",
        expectedRole: "Viewer",
        restrictedAction: "Edit Content",
        accessResult: "Action is blocked with 403 Forbidden message"
      },
      {
        loginUrl: "https://app.example.com/login",
        username: "[email protected]",
        password: "GuestP@ss1",
        landingPage: "/public/home",
        targetSection: "Public Pages",
        expectedTitle: "Public Information",
        expectedRole: "Guest",
        restrictedAction: "View Reports",
        accessResult: "Action is blocked and user is redirected to upgrade page"
      }
    ];

    var parameterXml = buildParameterXml(parameterNames, parameterRows);

    return createWorkItem("Test Case", [
      { field: "System.Title", value: "Verify Role-Based Access Control Across User Types" },
      {
        field: "System.Description",
        value: "Parameterized test verifying that each user role has correct access permissions. " +
          "Uses shared login steps and iterates across four user types."
      },
      { field: "Microsoft.VSTS.TCM.Steps", value: testSteps },
      { field: "Microsoft.VSTS.TCM.LocalDataSource", value: parameterXml },
      { field: "Microsoft.VSTS.Common.Priority", value: 1 }
    ]);
  })
  .then(function(testCase) {
    var testCaseId = testCase.id;
    console.log("Test case created with ID: " + testCaseId);

    // Step 3: Add test case to the suite
    console.log("\n--- Adding Test Case to Suite ---");
    return addToTestSuite(testCaseId);
  })
  .then(function() {
    console.log("Test case added to suite " + config.testSuiteId);

    // Step 4: Retrieve and display the execution matrix
    console.log("\n--- Test Execution Matrix ---");
    return getTestPoints();
  })
  .then(function(points) {
    if (!points || points.length === 0) {
      console.log("No test points found. Points are generated when the suite is queried.");
      return;
    }

    console.log("Total test points: " + points.length);
    console.log("");

    points.forEach(function(point) {
      console.log("Point #" + point.id +
        " | Test Case: " + point.testCase.id +
        " | Config: " + (point.configuration ? point.configuration.name : "Default") +
        " | State: " + point.outcome);
    });

    console.log("\n--- Execution Matrix Summary ---");
    var configs = {};
    points.forEach(function(point) {
      var configName = point.configuration ? point.configuration.name : "Default";
      if (!configs[configName]) {
        configs[configName] = { total: 0, passed: 0, failed: 0, active: 0 };
      }
      configs[configName].total++;
      if (point.outcome === "Passed") configs[configName].passed++;
      else if (point.outcome === "Failed") configs[configName].failed++;
      else configs[configName].active++;
    });

    Object.keys(configs).forEach(function(name) {
      var c = configs[name];
      console.log(name + ": " + c.total + " points (" +
        c.passed + " passed, " + c.failed + " failed, " + c.active + " active)");
    });
  })
  .catch(function(err) {
    if (err.response) {
      console.error("API Error " + err.response.status + ":", JSON.stringify(err.response.data, null, 2));
    } else {
      console.error("Error:", err.message);
    }
    process.exit(1);
  });
}

main();

Save this as parameterized-test-setup.js, set your environment variables, and run:

export AZURE_DEVOPS_PAT="your-pat-token"
export AZURE_ORG="your-organization"
export AZURE_PROJECT="your-project"
export TEST_PLAN_ID="100"
export TEST_SUITE_ID="200"

node parameterized-test-setup.js

The script creates a shared step for the login procedure, a parameterized test case with four user role iterations that references the shared step, adds the test case to a suite, and retrieves the resulting test execution matrix.

Common Issues and Troubleshooting

Parameter Values Not Substituting During Execution

This happens when the parameter name in the step text does not exactly match a column name in the data table. The most common cause is a case mismatch or a typo. Steps use @userName but the data table column is username. Azure DevOps parameter matching is case-insensitive, but the column must exist. Check the LocalDataSource XML to verify column names match the @ references in your steps.

Shared Step Parameters Not Appearing in Test Case Data Table

When you insert a shared step into a test case, the shared step's parameters should merge into the test case's data grid. If they do not appear, the test case may need to be saved and reopened. In some cases, you need to manually add the parameter columns to the test case's data table and populate them. This is a known UI quirk — the API handles it more reliably because you control the LocalDataSource XML directly.

"The field 'Microsoft.VSTS.TCM.Steps' cannot be updated" Error

This 400 error from the REST API usually means the XML in the Steps field is malformed. The steps XML format is strict — every <step> must have exactly two <parameterizedString> children (action and expected result), and <compref> elements must reference valid shared step work item IDs. Validate your XML structure against the schema before sending it. A missing closing tag or unescaped ampersand in step text will cause this error.

Iteration Count Does Not Match Data Rows

If the Test Runner shows a different number of iterations than you have data rows, there may be a mismatch between the data table and the assigned configurations. Each configuration multiplies the iteration count. If you have 4 data rows and 2 configurations, you get 8 test points. Remove unneeded configuration assignments from the test suite to reduce the matrix. Also check that the LocalDataSource XML is well-formed — corrupt XML can cause Azure DevOps to ignore some or all rows.

Shared Step Reference Breaks After Work Item Move

Moving a shared step work item to a different project changes its ID in some scenarios (especially cross-project moves). All test cases referencing the old ID will show broken shared step references. Before moving shared steps, identify all referencing test cases using a WIQL query:

SELECT [System.Id] FROM WorkItems
WHERE [System.WorkItemType] = 'Test Case'
AND [Microsoft.VSTS.TCM.Steps] CONTAINS 'ref="4521"'

Update the references after the move, or recreate the shared step in the target project and use the API to update all referencing test cases.

Best Practices

  • Keep parameter data tables focused. Do not overload a single test case with thirty parameter rows covering every edge case. Use separate test cases for boundary testing, happy path, and negative scenarios. Five to ten rows per test case is a practical limit before readability suffers.

  • Name shared steps with a consistent prefix. Use prefixes like "Login -", "Setup -", or "Verify -" so shared steps are easy to find when searching. A naming convention like "[Module] Action Description" works well: "[Auth] Login as standard user", "[Checkout] Add item to cart".

  • Audit shared step usage before editing. Before modifying a shared step, query which test cases reference it. A seemingly minor change can break dozens of test cases. The REST API query [Microsoft.VSTS.TCM.Steps] CONTAINS 'ref="<id>"' gives you the impact radius.

  • Version your parameter data externally. Store parameter data tables in CSV or JSON files in your repository. Use the REST API to sync them to Azure DevOps. This gives you version control, code review, and diff visibility for test data changes — none of which you get from editing data tables in the web UI.

  • Use shared steps for preconditions, not assertions. Shared steps work best for setup procedures (login, navigation, data creation) that are truly identical across test cases. Verification steps are usually test-specific. Forcing verification into shared steps leads to overly generic assertions that miss case-specific validation.

  • Do not nest shared steps more than one level deep. Azure DevOps supports shared steps referencing other shared steps, but nesting makes test execution confusing and debugging difficult. If your shared step needs another shared step, flatten the hierarchy. A single level of shared step references is the maintainability sweet spot.

  • Clean up orphaned shared steps quarterly. Shared steps accumulate over time as test cases are deleted or rewritten. Run periodic queries to find shared steps with zero references and archive or delete them. Orphaned shared steps clutter search results and confuse new team members.

  • Separate test data from test logic. Parameters handle data variation. Steps handle behavioral verification. Do not encode logic decisions in parameter values. A parameter value of "Click the red button if admin, blue button if viewer" belongs in separate test cases, not in a data row.

References

Powered by Contentful