Once the basics are in place, a few lesser-known TestNG features solve specific problems neatly: @Factory runs a whole test class once per configuration, invocationCount repeats a test (optionally in parallel), and attributes like timeOut, enabled, description and singleThreaded control how tests run. This guide shows each with code, plus the ITestResult status codes you use when reacting to results.

@Factory: Creating Test Instances Dynamically

@Factory creates multiple instances of a test class. Each instance can be configured differently. Useful for running the same tests with different configurations (e.g., different users, different browsers).
// LoginTest.java — parameterised test class
public class LoginTest {
    private String username;
    private String password;
    private String expectedRole;
    // Constructor — one instance per user
    public LoginTest(String username, String password, String role) {
        this.username    = username;
        this.password    = password;
        this.expectedRole = role;
    }
    @Test
    public void testLogin() {
        loginPage.login(username, password);
        Assert.assertEquals(dashboardPage.getUserRole(), expectedRole);
    }
    @Test(dependsOnMethods = {"testLogin"})
    public void testDashboardAccess() {
        Assert.assertTrue(dashboardPage.isAccessible(expectedRole));
    }
}
// LoginTestFactory.java — creates instances
public class LoginTestFactory {
    @Factory
    public Object[] createInstances() {
        return new Object[] {
            new LoginTest("admin@test.com",   "Admin@123",   "ADMIN"),
            new LoginTest("manager@test.com", "Manager@123", "MANAGER"),
            new LoginTest("user@test.com",    "User@123",    "USER"),
        };
        // TestNG creates 3 instances — each runs all @Test methods
    }
}
// testng.xml — only include the factory class
<class name="tests.LoginTestFactory"/>

@Factory vs @DataProvider: a DataProvider runs one method once per data row; a factory creates whole instances, so every test method in the class runs for each configuration (for example each user role or each browser).

Advertisement

invocationCount and threadPoolSize

// Run test 10 times sequentially
@Test(invocationCount = 10)
public void testLoginStability() {
    loginPage.login("admin", "admin123");
    Assert.assertTrue(homePage.isDisplayed(), "Login failed");
    driver.navigate().back();
}
// Run 10 times using 3 parallel threads
@Test(invocationCount = 10, threadPoolSize = 3)
public void testConcurrentLogin() {
    // 10 login attempts run concurrently, 3 at a time
    loginPage.login("user" + Thread.currentThread().getId(), "pass");
}
// Get current invocation number
@Test(invocationCount = 5)
public void testWithCount() {
    int count = Reporter.getCurrentTestResult().getMethod().getCurrentInvocationCount();
    System.out.println("Invocation: " + count);
}

invocationCount with a thread pool is handy for quick concurrency checks, such as several users logging in at once, but it isn't a load test; use JMeter or k6 for real performance testing.

Test Execution Control: enabled, timeOut, description, singleThreaded

// @Test(enabled = false) — skip test with reason
@Test(enabled = false)
public void testOldFeature() {
    // Disabled: not run and not shown in the results
}
// @Test(timeOut) — fail if test takes too long
@Test(timeOut = 5000)  // fails if > 5 seconds
public void testPageLoad() {
    driver.get("https://myapp.com/heavy-page");
    Assert.assertTrue(page.isLoaded());
}
// @Test(description) — adds metadata to reports
@Test(description = "TC_001: Verify login with valid credentials — Smoke Test")
public void TC_001_validLogin() { ... }
// @Test(singleThreaded = true) — class runs in one thread even in parallel suite
@Test(singleThreaded = true)  // class-level attribute: put it on the class
public class OrderedTest {
    @Test(priority = 1) public void step1() { ... }
    @Test(priority = 2) public void step2() { ... }
    @Test(priority = 3) public void step3() { ... }
}

ITestResult Status Codes

// Status constants in ITestResult
ITestResult.SUCCESS  = 1   // test passed
ITestResult.FAILURE  = 2   // test failed
ITestResult.SKIP     = 3   // test skipped
ITestResult.SUCCESS_PERCENTAGE_FAILURE = 4  // partial success
ITestResult.STARTED  = 16  // test started (not completed)
// Usage in @AfterMethod
@AfterMethod
public void handleResult(ITestResult result) {
    switch (result.getStatus()) {
        case ITestResult.SUCCESS: logger.info("PASS: " + result.getName()); break;
        case ITestResult.FAILURE: captureScreenshot(); break;
        case ITestResult.SKIP:    logger.warn("SKIP: " + result.getName()); break;
    }
}

FAQs

What is @Factory in TestNG?

A method annotated with @Factory returns an array of test-class instances, and TestNG runs all test methods in each instance, which is useful for running the same tests with different users, browsers or environments.

What is the difference between @Factory and @DataProvider?

A DataProvider supplies parameters to a single test method; a factory creates multiple instances of a test class so that every method in the class runs per configuration.

What does invocationCount do in TestNG?

It runs the same test method several times. Adding threadPoolSize runs those invocations in parallel threads.

What happens when a test exceeds timeOut?

TestNG marks it as failed with a ThreadTimeoutException once the time in milliseconds is exceeded.