The @Test annotation is not just a marker. It has attributes that give you fine-grained control over how tests run. Think of @Test as a shipping label — you can add a priority number, a destination group, dependencies, and a time limit.
By default, TestNG runs @Test methods in alphabetical order. Use priority to set a specific order. Lower number runs first. Default priority is 0.
public class PriorityDemo {
@Test // priority = 0 (default) — runs FIRST
public void testHomePage() {
System.out.println("0. Home page loads");
}
@Test(priority = 1)
public void testLogin() {
System.out.println("1. Login works");
}
@Test(priority = 2)
public void testDashboard() {
System.out.println("2. Dashboard loads after login");
}
@Test(priority = 3)
public void testLogout() {
System.out.println("3. Logout works");
}
}
// Output order: testHomePage → testLogin → testDashboard → testLogoutTests with the same priority run in alphabetical order. So if testZebra and testApple both have priority = 1, testApple runs first. Do not rely on alphabetical order — always set explicit priorities when order matters.
In real projects, you have thousands of tests. You do not run all of them every time. Groups let you tag tests and run only what you need:
public class BankingTests {
@Test(groups = {"smoke"})
public void testLoginPageLoads() {
// Quick check — runs in every build
}
@Test(groups = {"smoke", "regression"})
public void testSuccessfulLogin() {
// Core flow — part of both smoke and regression
}
@Test(groups = {"regression"})
public void testLoginWithSpecialCharacters() {
// Edge case — only runs in full regression
}
@Test(groups = {"regression"})
public void testPasswordResetLink() {
// Only runs in regression
}
@Test(groups = {"broken"}, enabled = false)
public void testForgotPassword() {
// Known broken feature — disabled until fix
}
}In most companies, QA teams have at least three groups: "smoke" (5-10 critical tests, run on every build), "regression" (full suite, run nightly), and "sanity" (quick check after deployment). Define your groups early and stick to them.
Sometimes a test only makes sense if another test passed. For example, you cannot test the dashboard if login failed. dependsOnMethods creates this dependency. If the parent test fails, the dependent test is SKIPPED (not failed).
public class DependencyDemo {
@Test
public void testLogin() {
System.out.println("Login test executing...");
// If this FAILS → testDashboard and testTransfer are SKIPPED
}
@Test(dependsOnMethods = {"testLogin"})
public void testDashboard() {
System.out.println("Dashboard test — only runs if login passed");
}
@Test(dependsOnMethods = {"testLogin"})
public void testTransfer() {
System.out.println("Transfer test — only runs if login passed");
}
@Test(dependsOnMethods = {"testDashboard"})
public void testTransactionHistory() {
System.out.println("History — only runs if dashboard passed");
}
}Use dependsOnMethods sparingly. If every test depends on the previous one, you have a chain where one failure skips everything. Independent tests are better. Only use dependencies when there is a genuine logical dependency (you truly cannot test the dashboard without being logged in).
Sets a maximum time in milliseconds. If the test takes longer, it fails. Useful for catching performance regressions.
@Test(timeOut = 5000) // Must finish within 5 seconds
public void testDashboardLoadsQuickly() {
driver.get("https://www.testerrank.com/banking/dashboard");
wait.until(ExpectedConditions.visibilityOfElementLocated(
By.id("balance")
));
// If this takes more than 5 seconds → test FAILS
}
@Test(timeOut = 30000) // 30 seconds for a complex flow
public void testFullTransferFlow() {
// Login → Navigate → Fill form → Submit → Verify
}// enabled — Temporarily disable a test without deleting it
@Test(enabled = false)
public void testBrokenFeature() {
// Disabled until the bug is fixed
}
// description — Shows in reports, makes tests self-documenting
@Test(description = "Verify login succeeds with valid credentials")
public void testSuccessfulLogin() { }
// invocationCount — Run the same test multiple times (flakiness check)
@Test(invocationCount = 5)
public void testLoginReliability() {
// Runs 5 times — if it fails even once, you have a flaky test
}
// invocationCount + threadPoolSize — Basic load test
@Test(invocationCount = 10, threadPoolSize = 3)
public void testConcurrentAccess() {
// 10 runs across 3 threads simultaneously
}
// expectedExceptions — Test PASSES if the exception is thrown
@Test(expectedExceptions = ArithmeticException.class)
public void testDivisionByZero() {
int result = 10 / 0; // Throws ArithmeticException → test PASSES
}| Attribute | Type | Default | What It Does |
|---|---|---|---|
| priority | int | 0 | Execution order — lower runs first |
| groups | String[] | {} | Assign to smoke, regression, etc. |
| dependsOnMethods | String[] | {} | Run only if these methods pass |
| dependsOnGroups | String[] | {} | Run only if these groups pass |
| enabled | boolean | true | Set false to skip the test |
| timeOut | long (ms) | 0 | Max time — fails if exceeded |
| invocationCount | int | 1 | How many times to run |
| threadPoolSize | int | 0 | Threads for invocationCount runs |
| description | String | "" | Shows in reports |
| expectedExceptions | Class[] | {} | Test passes if exception is thrown |
Q: What @Test attributes have you used and why?
A: priority for controlling execution order (login before dashboard), groups for tagging tests as smoke or regression, dependsOnMethods when there is a logical dependency (dashboard depends on login), timeOut for performance assertions, description for readable reports, and enabled=false to temporarily skip broken tests without deleting code. I also use invocationCount to check for flaky tests during development.
Key Point: The @Test annotation has attributes like priority, groups, dependsOnMethods, timeOut, and enabled that control test execution.