Test Groups and Dependencies

9 min read

A folder of 200 test classes is not a test strategy — it's a pile. Groups turn that pile into a deliberate set of overlapping cuts: a 10-test smoke run that gates every commit, a 200-test regression run that runs on merge, a slow cross-browser suite that runs nightly. Dependencies tell TestNG "this test can only run if that test passed" — and when the prerequisite breaks, downstream tests are skipped, not failed, which is the honest signal for "nothing was verified here." This lesson covers both, starting with groups because they shape your entire suite architecture.

Tagging tests with groups

A @Test method can belong to one group, many groups, or no group at all:

package com.mycompany.tests.tests;
 
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.testng.Assert;
import org.testng.annotations.Test;
 
public class LoginTest extends com.mycompany.tests.base.BaseTest {
 
    @Test(groups = {"smoke", "regression"},
          description = "Valid credentials land on the inventory page")
    public void validLoginSucceeds() {
        getDriver().findElement(By.id("user-name")).sendKeys("standard_user");
        getDriver().findElement(By.id("password")).sendKeys("secret_sauce");
        getDriver().findElement(By.id("login-button")).click();
        Assert.assertTrue(getDriver().getCurrentUrl().contains("/inventory.html"));
    }
 
    @Test(groups = {"regression"},
          description = "Wrong password shows the error banner")
    public void invalidCredentialsShowError() {
        getDriver().findElement(By.id("user-name")).sendKeys("standard_user");
        getDriver().findElement(By.id("password")).sendKeys("wrong");
        getDriver().findElement(By.id("login-button")).click();
        Assert.assertTrue(
            getDriver().findElement(By.cssSelector("[data-test='error']")).isDisplayed()
        );
    }
 
    @Test(groups = {"regression", "slow"},
          description = "Brute-force lockout after multiple failures")
    public void accountLocksAfterFailures() {
        for (int i = 0; i < 5; i++) {
            getDriver().findElement(By.id("user-name")).sendKeys("locked_out_user");
            getDriver().findElement(By.id("password")).sendKeys("wrong");
            getDriver().findElement(By.id("login-button")).click();
            getDriver().navigate().back();
        }
        Assert.assertTrue(
            getDriver().findElement(By.cssSelector("[data-test='error']"))
                .getText().contains("locked out")
        );
    }
}

validLoginSucceeds is in both smoke and regression — it runs in every suite. invalidCredentialsShowError is regression-only. accountLocksAfterFailures is tagged slow so it can be excluded from time-sensitive runs.

Controlling groups in testng.xml

<!-- smoke.xml — runs only smoke-tagged tests -->
<suite name="Smoke" parallel="methods" thread-count="4">
    <test name="Smoke">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <packages>
            <package name="com.mycompany.tests.tests"/>
        </packages>
    </test>
</suite>
 
<!-- regression.xml — everything except slow and wip -->
<suite name="Regression" parallel="methods" thread-count="4">
    <test name="Regression">
        <groups>
            <run>
                <include name="regression"/>
                <exclude name="slow"/>
                <exclude name="wip"/>
            </run>
        </groups>
        <packages>
            <package name="com.mycompany.tests.tests"/>
        </packages>
    </test>
</suite>
 
<!-- nightly.xml — everything including slow, nothing excluded -->
<suite name="Nightly" parallel="methods" thread-count="2">
    <test name="Full">
        <groups>
            <run>
                <include name="regression"/>
            </run>
        </groups>
        <packages>
            <package name="com.mycompany.tests.tests"/>
        </packages>
    </test>
</suite>

Three suites, same Java source, different groups included. CI pipelines run smoke on every push, regression on merge to main, nightly on a schedule.

Run a specific group from the CLI without editing XML:

mvn test -Dgroups=smoke
mvn test -Dgroups="regression,smoke"
mvn test -Dexcludedgroups=slow

@BeforeGroups and @AfterGroups

Some groups need group-specific setup — database seeding for tests that hit the DB, WireMock stubs for tests that make API calls:

@BeforeGroups("database")
public void seedDatabase() {
    System.out.println("Seeding test database for 'database' group tests");
    // insert test data
}
 
@Test(groups = {"database", "regression"})
public void canQueryUserById() {
    // test that reads from the database
}
 
@AfterGroups("database")
public void cleanDatabase() {
    System.out.println("Cleaning test database");
    // remove test data
}

@BeforeGroups fires once before the first test in that group runs. @AfterGroups fires once after the last test in that group finishes. Use these for expensive group-specific resources — don't reach for them for cheap per-test setup (that belongs in @BeforeMethod).

Method dependencies with dependsOnMethods

dependsOnMethods tells TestNG: "only run this test if the named method passed."

public class CheckoutFlowTest extends com.mycompany.tests.base.BaseTest {
 
    @Test(groups = {"smoke", "regression"},
          description = "Log in to set up session for downstream tests")
    public void loginSucceeds() {
        getDriver().findElement(By.id("user-name")).sendKeys("standard_user");
        getDriver().findElement(By.id("password")).sendKeys("secret_sauce");
        getDriver().findElement(By.id("login-button")).click();
        Assert.assertTrue(getDriver().getCurrentUrl().contains("/inventory.html"));
    }
 
    @Test(groups = {"regression"},
          dependsOnMethods = {"loginSucceeds"},
          description = "Add item to cart — requires active session")
    public void addItemToCart() {
        getDriver().findElement(
            By.cssSelector("[data-test='add-to-cart-sauce-labs-backpack']")
        ).click();
        Assert.assertEquals(
            getDriver().findElement(By.cssSelector(".shopping_cart_badge")).getText(),
            "1"
        );
    }
 
    @Test(groups = {"regression"},
          dependsOnMethods = {"addItemToCart"},
          description = "Proceed to checkout — requires cart to have an item")
    public void checkoutStartsSuccessfully() {
        getDriver().findElement(By.cssSelector(".shopping_cart_link")).click();
        getDriver().findElement(By.id("checkout")).click();
        Assert.assertTrue(getDriver().getCurrentUrl().contains("/checkout-step-one.html"));
    }
}

If loginSucceeds fails, addItemToCart is skipped — not failed. If addItemToCart is skipped, checkoutStartsSuccessfully is also skipped. The report shows three items: FAILED, SKIPPED, SKIPPED. That is the honest picture — only the root cause failed; the rest had no chance to run.

Compare to the alternative (no dependency): all three tests try to run, and addItemToCart fails with NoSuchElementException because the user isn't logged in. Now you have three FAILED tests, and the real failure is hidden among two misleading ones.

Group dependencies between groups

<groups>
    <dependencies>
        <group name="regression" depends-on="smoke"/>
    </dependencies>
</groups>

If any test in the smoke group fails, ALL tests in regression are skipped. Use this to gate a slow full regression run behind a fast smoke gate — if smoke is broken, running all 200 regression tests wastes time and produces a noisy failure report.

Groups as a concept map

Test Groups
  • – ~5–15 core tests
  • – Runs in 2–3 minutes
  • – Gates every commit to main
  • – Both smoke AND regression tagged
  • – Full coverage — all edge cases
  • – Runs on PR merge or nightly
  • – Includes smoke tests (superset)
  • – Excludes slow and wip
  • – Multi-step E2E flows
  • – Cross-browser runs
  • – Excluded from smoke and fast regression
  • – Included in nightly suite only
  • In-progress or flaky tests –
  • Always excluded in CI –
  • Explicitly included for local dev –
  • Tag flaky, fix, then promote to regression –

Use dependencies sparingly

Independent tests that each set up their own state are almost always better than chains of dependent tests. Dependencies have two costs:

  1. They kill parallelism. TestNG cannot run addItemToCart until loginSucceeds finishes — even if other tests are waiting for a thread.
  2. They cascade. Break loginSucceeds and every downstream test skips. In a long chain, one failure suppresses 20 tests.

Reserve dependsOnMethods for cases where setup is genuinely too expensive to repeat in every test, or where the dependency is a fundamental logical prerequisite (you cannot test the cart without being logged in — it's not possible, not just inconvenient).

⚠️ Common mistakes

  • Using dependsOnMethods to force execution order instead of for logical prerequisites. If you depend on a test just to control order, use priority instead. Dependencies create brittle skip cascades; priorities just sort the run order without any skip behavior.
  • Not treating a wave of SKIPs as a failure signal. TestNG distinguishes SKIP from FAIL, but a report full of skipped tests means those tests verified nothing. Teams sometimes see "no failures" in CI and miss that 30 tests were skipped because a prerequisite broke. Monitor skip counts alongside failure counts.
  • Tagging every test with regression and no test with smoke. The group system is only useful if you actually differentiate. "Smoke" means the 10 tests that verify the system is basically working — if any of those fail, stop everything. Choose them carefully.

🎯 Practice task

Slice your suite with groups. 30–40 minutes.

  1. Go through your existing test methods and add groups = {"smoke"} or groups = {"regression"} to each. Pick 2–3 tests to be both smoke and regression. Mark any unstable test groups = {"wip"}.
  2. Create three suite files: smoke.xml, regression.xml, nightly.xml. nightly.xml includes regression and slow, excludes nothing. Run each and confirm the right subset executes.
  3. Test the skip cascade. Create a three-test chain using dependsOnMethods. Make the first test fail intentionally. Run — confirm the second and third show as SKIPPED in the report. Restore the first test.
  4. Try @BeforeGroups / @AfterGroups. Tag three tests groups = {"db"}. Add @BeforeGroups("db") that prints "DB setup" and @AfterGroups("db") that prints "DB cleanup". Run and confirm the output shows exactly one setup and one cleanup line.
  5. CLI group filtering. Run mvn test -Dgroups=smoke. Confirm only smoke-tagged tests run. Run mvn test -Dexcludedgroups=wip. Confirm wip tests are excluded.
  6. Stretch — group dependency in XML. Add a <dependencies> block to regression.xml that makes regression depend on smoke. Force a smoke test to fail. Run regression.xml — the regression tests should all skip cleanly.

Next lesson: parameters from testng.xml — injecting environment config (base URL, browser, timeouts) into your tests without changing a line of Java.

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