Parameters from testng.xml

8 min read

Every serious test suite has at least one thing that changes between environments: the base URL, the browser, the API key, the timeout. Hardcoding any of those into Java forces you to edit source code to change the target environment — which means recompiling, and means developers can accidentally check in production credentials. @Parameters and testng.xml solve this cleanly: environment-specific values live in the XML file (or come from CLI system properties), and the Java test code reads them at runtime with no recompilation required. This lesson covers the full parameter system, the resolution order that decides where a value actually comes from, and how to combine XML parameters with system properties for maximum flexibility.

The basic @Parameters pattern

Declare parameters in testng.xml:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parameterised Suite" verbose="1">
 
    <test name="Staging Tests">
        <parameter name="baseUrl"  value="https://staging.myapp.com"/>
        <parameter name="browser"  value="chrome"/>
        <parameter name="timeout"  value="10"/>
        <classes>
            <class name="com.mycompany.tests.tests.LoginTest"/>
            <class name="com.mycompany.tests.tests.ProductTest"/>
        </classes>
    </test>
 
    <test name="Production Tests">
        <parameter name="baseUrl"  value="https://myapp.com"/>
        <parameter name="browser"  value="chrome"/>
        <parameter name="timeout"  value="5"/>
        <classes>
            <class name="com.mycompany.tests.tests.LoginTest"/>
            <class name="com.mycompany.tests.tests.ProductTest"/>
        </classes>
    </test>
 
</suite>

Inject them into test methods or configuration methods with @Parameters:

package com.mycompany.tests.base;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.*;
 
public class BaseTest {
 
    protected WebDriver driver;
    protected String baseUrl;
    protected int timeout;
 
    @BeforeMethod
    @Parameters({"baseUrl", "browser", "timeout"})
    public void setup(
            String baseUrl,
            String browser,
            @Optional("10") String timeout) {
 
        this.baseUrl = baseUrl;
        this.timeout = Integer.parseInt(timeout);
 
        WebDriverManager.chromedriver().setup();
        switch (browser.toLowerCase()) {
            case "firefox" -> {
                WebDriverManager.firefoxdriver().setup();
                driver = new FirefoxDriver();
            }
            case "edge" -> {
                WebDriverManager.edgedriver().setup();
                driver = new org.openqa.selenium.edge.EdgeDriver();
            }
            default -> driver = new ChromeDriver();
        }
 
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(
            java.time.Duration.ofSeconds(this.timeout)
        );
        driver.get(this.baseUrl);
    }
 
    @AfterMethod(alwaysRun = true)
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

The same LoginTest.java and ProductTest.java now run against staging with a 10-second timeout and production with a 5-second timeout — zero Java changes.

@Optional — default values

@Optional("chrome") provides a default when no matching <parameter> exists in the XML. This matters in two situations:

  1. Running a single test class from IntelliJ (no testng.xml is used, so no parameters are injected)
  2. A <test> block that inherits from the <suite> level but doesn't override every parameter
@BeforeMethod
@Parameters({"browser", "baseUrl"})
public void setup(
        @Optional("chrome") String browser,
        @Optional("http://localhost:3000") String baseUrl) {
    // Works even when called outside a suite
}

Without @Optional, TestNG throws org.testng.TestNGException: Parameter 'browser' is required by @Parameters on method setup but has not been defined when the parameter is missing. Add @Optional to every parameter that might be absent when running outside a complete suite.

Parameters at different scope levels

Parameters can be declared at the <suite> level (available everywhere) or at the <test> level (overrides the suite value for that block):

<suite name="Multi-env">
    <!-- Default: all tests use chrome unless overridden -->
    <parameter name="browser" value="chrome"/>
 
    <test name="Staging">
        <!-- Override base URL for staging -->
        <parameter name="baseUrl" value="https://staging.myapp.com"/>
        <packages><package name="com.mycompany.tests.tests"/></packages>
    </test>
 
    <test name="Production">
        <!-- Override both for production -->
        <parameter name="baseUrl" value="https://myapp.com"/>
        <parameter name="browser" value="edge"/>
        <packages><package name="com.mycompany.tests.tests"/></packages>
    </test>
</suite>

The staging <test> block inherits browser=chrome from the suite and overrides baseUrl. The production block overrides both.

Parameters vs DataProviders

These two mechanisms solve different problems and are often confused:

@Parameters — static values from XML, same for every test in a <test> block. Right choice for:

  • Environment config: base URL, API host, database connection string
  • Browser choice: chrome, firefox, edge
  • Suite-level timeouts and settings

@DataProvider — dynamic data returned from a Java method, different per test invocation. Right choice for:

  • Login scenarios: 10 different username/password pairs
  • Product data: 50 different SKUs to verify
  • Input validation: 20 edge-case strings

Never use @Parameters for test data (it can only inject one value at a time per parameter name). Never use @DataProvider for environment config (it runs at the method level, not the suite level). Chapter 3 covers @DataProvider in depth.

System properties as an override layer

System properties let you override parameters without editing XML — essential for CI pipelines:

mvn test -DbaseUrl=https://uat.myapp.com -Dbrowser=firefox

Combined with @Parameters, you can read both:

@BeforeMethod
@Parameters({"baseUrl", "browser"})
public void setup(
        @Optional("http://localhost:3000") String xmlBaseUrl,
        @Optional("chrome") String xmlBrowser) {
 
    // System property wins; XML parameter is the fallback
    String baseUrl = System.getProperty("baseUrl", xmlBaseUrl);
    String browser = System.getProperty("browser", xmlBrowser);
 
    System.out.println("Running against: " + baseUrl + " on " + browser);
    // ... driver setup
}

This is the pattern used in most professional frameworks: testng.xml carries the default environment, and the CI pipeline can override any parameter with -D flags without anyone editing files.

Parameter resolution order

A complete parameterised test

package com.mycompany.tests.tests;
 
import com.mycompany.tests.base.BaseTest;
import org.openqa.selenium.By;
import org.testng.Assert;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
 
public class LoginTest extends BaseTest {
 
    // baseUrl and driver are injected by BaseTest.setup()
 
    @Test(groups = {"smoke", "regression"},
          description = "Valid credentials land on the inventory page")
    public void validLoginSucceeds() {
        driver.findElement(By.id("user-name")).sendKeys("standard_user");
        driver.findElement(By.id("password")).sendKeys("secret_sauce");
        driver.findElement(By.id("login-button")).click();
        Assert.assertTrue(driver.getCurrentUrl().contains("/inventory.html"),
            "Expected inventory URL after login");
    }
 
    @Test(groups = {"regression"},
          description = "Locked user sees the locked-out error message")
    public void lockedUserCannotLogin() {
        driver.findElement(By.id("user-name")).sendKeys("locked_out_user");
        driver.findElement(By.id("password")).sendKeys("secret_sauce");
        driver.findElement(By.id("login-button")).click();
        String error = driver.findElement(By.cssSelector("[data-test='error']")).getText();
        Assert.assertTrue(error.contains("locked out"), "Expected locked-out message");
    }
}

LoginTest has no hardcoded URL or browser name. It reads whatever BaseTest.setup() was given from the XML (or the CLI override). Run it against staging, production, or localhost without touching LoginTest.java.

⚠️ Common mistakes

  • Forgetting @Optional on parameters injected by @BeforeMethod. When a developer runs LoginTest from IntelliJ by right-clicking the class — not via testng.xml — TestNG finds no parameters to inject. Without @Optional, it throws Parameter 'browser' is required. The fix is one annotation: @Optional("chrome"). Add it to every parameter that may be absent.
  • Using @Parameters for test data. A @Parameters({"username", "password"}) on a @Test method means only one username/password pair can be injected from the XML. For multiple scenarios, that's @DataProvider. Trying to do data-driven testing through parameters leads to copy-pasted XML and one test class per scenario.
  • Hardcoding a URL in even one place. One hardcoded "https://production.myapp.com" in a base class means every developer who runs the suite locally accidentally hits production. Make it a rule: every URL, every environment-specific value, is a parameter or a system property.

🎯 Practice task

Run the same tests against two environments. 25–35 minutes.

  1. Create a testng.xml with two <test> blocks — Staging and Production (or localhost and staging if you don't have a production URL). Each block should have <parameter name="baseUrl" value="..."/> and <parameter name="browser" value="chrome"/>.
  2. Update BaseTest.@BeforeMethod to use @Parameters({"baseUrl", "browser"}) and add @Optional defaults. Print the active URL and browser to the console so you can see which block is running.
  3. Run the suite. Confirm in the console that the staging block runs first against its URL and the production block runs second against its URL.
  4. Test CLI override. Run mvn test -DbaseUrl=https://example.com. Confirm the console prints example.com for both blocks — the CLI flag overrides the XML.
  5. Test IntelliJ direct run. Right-click LoginTest and run it directly (no XML). Confirm @Optional defaults are used and the test doesn't throw TestNGException.
  6. Stretch — three browsers. Add three <test> blocks with browser=chrome, browser=firefox, browser=edge. Set parallel="tests" thread-count="3" at the suite level. Run — all three browsers should launch and run simultaneously.

Next chapter: data-driven testing. @DataProvider turns one test method into dozens of test invocations, each named and reported separately, pulling data from Java, CSV, Excel, and JSON.

// tip to track lessons you complete and pick up where you left off across devices.