Data Provider with Method Injection

8 min read

A @DataProvider is just a method — and like any method it can accept parameters. TestNG can inject two objects into a provider: the java.lang.reflect.Method representing the test method that called it, and the ITestContext carrying suite-level information. Method injection lets one provider serve multiple test methods with different data for each, switching on the method name, its annotations, or any metadata you attach. ITestContext injection lets the provider read environment parameters from testng.xml and load the right dataset for the active run. This lesson covers both, with practical patterns you'll use in real frameworks.

Injecting the calling Method

Declare Method method as the first parameter of your @DataProvider method:

package com.mycompany.tests.data;
 
import org.testng.annotations.DataProvider;
import java.lang.reflect.Method;
 
public class DynamicDataProvider {
 
    @DataProvider(name = "dynamicData")
    public Object[][] getData(Method method) {
        return switch (method.getName()) {
            case "testLogin" -> new Object[][] {
                {"admin@test.com",  "AdminPass123",  200},
                {"user@test.com",   "UserPass123",   200},
                {"wrong@test.com",  "BadPass",       401},
            };
            case "testRegistration" -> new Object[][] {
                {"Alice",  "alice@test.com",  "Password1!",  201},
                {"",       "empty@test.com",  "Password1!",  400},
                {"Bob",    "bob@test.com",    "",            400},
            };
            case "testPasswordReset" -> new Object[][] {
                {"registered@test.com",   200},
                {"unknown@test.com",      404},
            };
            default -> new Object[][] {};
        };
    }
}

The three test methods that share this provider:

package com.mycompany.tests.tests;
 
import com.mycompany.tests.data.DynamicDataProvider;
import org.testng.Assert;
import org.testng.annotations.Test;
 
public class AuthApiTest {
 
    @Test(dataProvider = "dynamicData",
          dataProviderClass = DynamicDataProvider.class,
          description = "Login with various credentials")
    public void testLogin(String email, String password, int expectedStatus) {
        System.out.printf("Login: %s → expected %d%n", email, expectedStatus);
        Assert.assertTrue(true);
    }
 
    @Test(dataProvider = "dynamicData",
          dataProviderClass = DynamicDataProvider.class,
          description = "Register with various name/email/password combos")
    public void testRegistration(String name, String email,
                                  String password, int expectedStatus) {
        System.out.printf("Register: %s <%s> → expected %d%n", name, email, expectedStatus);
        Assert.assertTrue(true);
    }
 
    @Test(dataProvider = "dynamicData",
          dataProviderClass = DynamicDataProvider.class,
          description = "Password reset for known and unknown emails")
    public void testPasswordReset(String email, int expectedStatus) {
        System.out.printf("Reset: %s → expected %d%n", email, expectedStatus);
        Assert.assertTrue(true);
    }
}

One provider, three methods, three different Object[][] shapes — and each shape matches the parameter list of the method that will receive it. TestNG injects the Method object before calling the provider, so the switch runs once per method and returns the right rows.

Injecting Method to read custom annotations

The real power of Method injection is reading custom annotations to drive data loading declaratively:

// Custom annotation that names the test data file
package com.mycompany.tests.annotation;
 
import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestDataFile {
    String value();        // path inside testdata/ directory
    String sheet() default "Sheet1"; // for Excel
}
// Tests annotate themselves with the file they need
@Test(dataProvider = "fileBackedData",
      dataProviderClass = DynamicDataProvider.class)
@TestDataFile("testdata/login-scenarios.json")
public void testLoginFromFile(String email, String password, int expectedStatus) {
    Assert.assertTrue(true);
}
// Provider reads the annotation and loads the right file
@DataProvider(name = "fileBackedData")
public Object[][] fileBackedData(Method method) throws Exception {
    TestDataFile annotation = method.getAnnotation(TestDataFile.class);
    if (annotation == null) {
        throw new IllegalStateException(
            method.getName() + " must be annotated with @TestDataFile");
    }
    String path = annotation.value();
    if (path.endsWith(".json")) {
        return DataReader.fromJson(path);
    } else if (path.endsWith(".csv")) {
        return DataReader.fromCsv(path);
    } else {
        return DataReader.fromExcel(path, annotation.sheet());
    }
}

Each test now declares its own data source via annotation. The provider is a generic dispatcher — it does not need to know about every test method by name.

Injecting ITestContext

ITestContext carries the current <test> block's configuration from testng.xml — including any <parameter> values. This lets the provider load environment-appropriate data:

import org.testng.ITestContext;
 
@DataProvider(name = "environmentAwareData")
public Object[][] environmentData(ITestContext context) throws Exception {
    String env = context.getCurrentXmlTest().getParameter("environment");
    if (env == null) env = "staging";
 
    String dataFile = switch (env) {
        case "production" -> "testdata/prod-users.json";
        case "staging"    -> "testdata/staging-users.json";
        default           -> "testdata/dev-users.json";
    };
 
    System.out.println("Loading data from: " + dataFile + " for env: " + env);
    return DataReader.fromJson(dataFile);
}

testng.xml with the environment parameter:

<suite name="Regression">
    <test name="Staging Tests">
        <parameter name="environment" value="staging"/>
        <classes>
            <class name="com.mycompany.tests.tests.AuthApiTest"/>
        </classes>
    </test>
</suite>

Run against production by changing the parameter value — or override from the CLI:

mvn test -Denvironment=production

Combining Method and ITestContext

Both can be injected together — declare them in this exact order:

@DataProvider(name = "fullInjectionData")
public Object[][] fullData(Method method, ITestContext context) throws Exception {
    String env = context.getCurrentXmlTest().getParameter("environment");
    TestDataFile annotation = method.getAnnotation(TestDataFile.class);
    String basePath = env + "/" + annotation.value();
    return DataReader.fromJson(basePath);
}

The injection flow

⚠️ Common mistakes

  • Declaring Method but not in the first parameter position. TestNG's injection contract requires Method as the first parameter, optionally followed by ITestContext. Swapping the order or adding other parameters before Method throws a provider invocation error at runtime.
  • Switching on method name as strings and misspelling one. "testLogin" vs "testlogin" returns an empty Object[][], and TestNG skips the test silently rather than failing loudly. Add a default case that throws an IllegalArgumentException("Unknown method: " + method.getName()) to catch typos immediately.
  • Reading ITestContext.getCurrentXmlTest() when running outside a suite. Running a single class from IntelliJ bypasses testng.xml entirely. getCurrentXmlTest() returns a synthetic context with no parameters. Always check context.getCurrentXmlTest().getParameter("key") for null before using it — or pair with an @Optional-style fallback in code.

🎯 Practice task

One provider, multiple methods. 25–35 minutes.

  1. Create DynamicDataProvider with getData(Method method) returning different data for three method names. Write the three test methods in AuthApiTest. Run — confirm each method receives its own rows.
  2. Test the default case. Add a fourth @Test that also uses dynamicData but whose name doesn't match any switch case. With the default -> new Object[][] {} return, TestNG either skips the test or runs zero invocations. Add a default -> throw new IllegalArgumentException(...) instead. Run — TestNG should now report a DataProviderHolder error naming the unregistered method.
  3. Custom annotation dispatch. Define @TestDataFile and add two JSON files to src/test/resources/testdata/. Write a fileBackedData provider that reads the annotation. Annotate two test methods with different files. Confirm each test loads from its own file.
  4. ITestContext environment switch. Add <parameter name="environment" value="staging"/> to testng.xml. Write a provider that reads the parameter and prints which data file it's loading. Verify the file path changes when you change the XML value.
  5. Stretch — combine both injections. Make a provider that takes both Method method and ITestContext context. Use the method name to pick a subdirectory and the environment parameter to pick a file within it. E.g., staging/login.json vs production/login.json.

Next lesson: @Factory — when you need multiple instances of the same test class with different constructor arguments, not just different method parameters.

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