# SkillPatch skill: testng-skill

This skill generates TestNG tests in Java, covering core patterns such as test groups, data providers, parallel execution, XML suite configuration, soft assertions, and listeners. It provides ready-to-use code snippets, lifecycle annotation ordering, anti-pattern guidance, and Maven CLI quick references. Designed to trigger when users mention TestNG-specific keywords like "@DataProvider", "testng.xml", or "parallel tests Java".

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/testng-skill
curl -sSL https://skillpatch.dev/install_skill/testng-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: testng-skill
description: >
  Generates TestNG tests in Java with groups, data providers, parallel execution,
  XML suite configuration, and listeners. Use when user mentions "TestNG",
  "@DataProvider", "testng.xml", "groups". Triggers on: "TestNG", "@DataProvider",
  "testng.xml", "TestNG suite", "parallel tests Java".
languages:
  - Java
category: unit-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# TestNG Testing Skill

## Core Patterns

### Basic Test with Groups

```java
import org.testng.annotations.*;
import org.testng.Assert;

public class LoginTest {
    @BeforeMethod
    public void setUp() { /* setup */ }

    @Test(groups = "smoke")
    public void testLoginSuccess() {
        Assert.assertTrue(loginService.login("user@test.com", "password123"));
    }

    @Test(groups = "regression", dependsOnMethods = "testLoginSuccess")
    public void testAccessDashboard() {
        Assert.assertNotNull(dashboard.getContent());
    }

    @Test(expectedExceptions = AuthenticationException.class)
    public void testLoginInvalidPassword() {
        loginService.login("user@test.com", "wrong");
    }

    @AfterMethod
    public void tearDown() { /* cleanup */ }
}
```

### Data Providers

```java
@DataProvider(name = "loginData")
public Object[][] loginData() {
    return new Object[][] {
        {"admin@test.com", "admin123", true},
        {"user@test.com", "password", true},
        {"invalid@test.com", "wrong", false},
    };
}

@Test(dataProvider = "loginData")
public void testLogin(String email, String password, boolean expected) {
    Assert.assertEquals(loginService.login(email, password), expected);
}
```

### TestNG XML Suite

```xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression" parallel="tests" thread-count="5">
  <test name="Smoke">
    <groups><run><include name="smoke"/></run></groups>
    <classes><class name="tests.LoginTest"/></classes>
  </test>
  <test name="Full">
    <groups><run><include name="regression"/><exclude name="flaky"/></run></groups>
    <packages><package name="tests.*"/></packages>
  </test>
</suite>
```

### Parallel Execution

```xml
<suite parallel="methods" thread-count="5">   <!-- Method level -->
<suite parallel="classes" thread-count="5">    <!-- Class level -->
<suite parallel="tests" thread-count="5">      <!-- Test level -->
```

### Soft Assertions

```java
SoftAssert soft = new SoftAssert();
soft.assertEquals(user.getName(), "Alice");
soft.assertEquals(user.getAge(), 25);
soft.assertTrue(user.isActive());
soft.assertAll();  // Reports all failures at once
```

### Listeners

```java
public class TestListener implements ITestListener {
    @Override public void onTestFailure(ITestResult result) {
        System.out.println("Failed: " + result.getName());
        // Take screenshot, log, etc.
    }
}

@Listeners(TestListener.class)
public class LoginTest { /* ... */ }
```

### Lifecycle Annotations

```
@BeforeSuite → @BeforeTest → @BeforeClass → @BeforeMethod → @Test → @AfterMethod → @AfterClass → @AfterTest → @AfterSuite
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| `dependsOnMethods` everywhere | Independent tests | Cascading failures |
| No groups | `@Test(groups = "smoke")` | Can't run subsets |
| Hard-coded test data | `@DataProvider` | Reusable |
| Priority ordering | Independent tests | Fragile |

## Quick Reference

| Task | Command |
|------|---------|
| Run suite | `mvn test -DsuiteXmlFile=testng.xml` |
| Run group | `mvn test -Dgroups=smoke` |
| Run class | `mvn test -Dtest=LoginTest` |
| Reports | `test-output/index.html` |

## Deep Patterns → `reference/playbook.md`

| § | Section | Lines |
|---|---------|-------|
| 1 | Project Setup & Configuration | Maven + Surefire config |
| 2 | Suite XML Configuration | Multi-env, parallel, groups |
| 3 | BaseTest & Thread-Safe Driver | ThreadLocal, ConfigReader |
| 4 | Data Providers (Advanced) | Excel, JSON, CSV, parallel, cross-class |
| 5 | Factory Pattern | Cross-browser matrix |
| 6 | Listeners (Production Suite) | Retry, screenshot, timing |
| 7 | Soft Assertions & Dependencies | Groups, method deps |
| 8 | Page Object Integration | PageFactory, fluent POs |
| 9 | Parallel Execution Strategies | Method/class/test/mixed |
| 10 | Reporting Integration | Allure, ExtentReports |
| 11 | CI/CD Integration | GitHub Actions, Jenkins |
| 12 | Debugging Quick-Reference | 12 common problems |
| 13 | Best Practices Checklist | 14 items |

````


### `reference/advanced-patterns.md`

````markdown
# TestNG — Advanced Patterns & Playbook

## Data Providers

```java
@DataProvider(name = "loginData", parallel = true)
public Object[][] loginData() {
    return new Object[][] {
        {"admin", "pass123", true},
        {"user", "wrong", false},
        {"", "", false}
    };
}

@Test(dataProvider = "loginData")
public void testLogin(String user, String pass, boolean expected) {
    assertEquals(authService.login(user, pass), expected);
}

// External data provider from Excel/CSV
@DataProvider(name = "excelData")
public Iterator<Object[]> excelData() throws Exception {
    return ExcelReader.read("test-data/users.xlsx").stream()
        .map(row -> new Object[]{row.get("name"), row.get("email")})
        .iterator();
}

// Factory pattern for dynamic test instances
@Factory(dataProvider = "browserData")
public BrowserTest(String browser, String version) {
    this.browser = browser; this.version = version;
}
@DataProvider
public static Object[][] browserData() {
    return new Object[][] {{"chrome","latest"},{"firefox","latest"}};
}
```

## Groups & Dependencies

```java
@Test(groups = {"smoke", "regression"})
public void testHomepage() { /* ... */ }

@Test(groups = "regression", dependsOnGroups = "smoke")
public void testCheckout() { /* ... */ }

@Test(dependsOnMethods = "testLogin", alwaysRun = true)
public void testLogout() { /* ... */ }

// Soft assertions — collect all failures
@Test
public void testUserProfile() {
    SoftAssert soft = new SoftAssert();
    User user = getUser(1);
    soft.assertEquals(user.getName(), "Alice");
    soft.assertEquals(user.getAge(), 30);
    soft.assertNotNull(user.getEmail());
    soft.assertAll(); // Report all failures at once
}
```

## Listeners

```java
public class RetryAnalyzer implements IRetryAnalyzer {
    private int count = 0;
    private static final int MAX_RETRY = 2;
    @Override
    public boolean retry(ITestResult result) {
        return count++ < MAX_RETRY;
    }
}

public class TestListener implements ITestListener {
    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("Starting: " + result.getName());
    }
    @Override
    public void onTestFailure(ITestResult result) {
        // Capture screenshot, log details
        Screenshot.capture(result.getName());
    }
}

// Apply globally
@Listeners({TestListener.class})
public class BaseTest { /* ... */ }
```

## Suite Configuration

```xml
<!-- testng.xml -->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression" parallel="methods" thread-count="4" verbose="1">
    <listeners>
        <listener class-name="com.test.TestListener"/>
    </listeners>
    <test name="Smoke">
        <groups><run><include name="smoke"/></run></groups>
        <classes><class name="com.test.SmokeTests"/></classes>
    </test>
    <test name="Full">
        <packages><package name="com.test.regression"/></packages>
    </test>
</suite>
```

## Anti-Patterns

- ❌ `@Test(priority = N)` for ordering — use `dependsOnMethods` or groups
- ❌ `Thread.sleep()` in tests — use explicit waits or polling
- ❌ `@Test(enabled = false)` committed — use groups to exclude instead
- ❌ Hardcoded test data in methods — use DataProviders
- ❌ Missing `SoftAssert.assertAll()` — silently passes with failures

````


### `reference/playbook.md`

````markdown
# TestNG — Advanced Implementation Playbook

## §1 — Project Setup & Configuration

```xml
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.18.1</version>
    </dependency>
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>5.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-testng</artifactId>
        <version>2.25.0</version>
    </dependency>
</dependencies>

<build><plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
        <configuration>
            <suiteXmlFiles><suiteXmlFile>testng.xml</suiteXmlFile></suiteXmlFiles>
            <parallel>classes</parallel>
            <threadCount>4</threadCount>
            <argLine>-Denv=${env:qa}</argLine>
        </configuration>
    </plugin>
</plugins></build>
```

## §2 — Suite XML Configuration

```xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression" parallel="classes" thread-count="4" verbose="1">
    <listeners>
        <listener class-name="listeners.RetryListener"/>
        <listener class-name="listeners.AllureListener"/>
        <listener class-name="listeners.ScreenshotListener"/>
    </listeners>

    <parameter name="browser" value="chrome"/>
    <parameter name="env" value="qa"/>

    <test name="Smoke">
        <groups>
            <run><include name="smoke"/></run>
        </groups>
        <classes>
            <class name="tests.LoginTest"/>
            <class name="tests.DashboardTest"/>
        </classes>
    </test>

    <test name="Regression">
        <groups>
            <run>
                <include name="regression"/>
                <exclude name="wip"/>
                <exclude name="flaky"/>
            </run>
        </groups>
        <packages><package name="tests.*"/></packages>
    </test>

    <test name="API" parallel="methods" thread-count="8">
        <classes><class name="tests.api.UserApiTest"/></classes>
    </test>
</suite>
```

### Multi-Environment Suites

```xml
<!-- testng-staging.xml -->
<suite name="Staging">
    <parameter name="env" value="staging"/>
    <parameter name="browser" value="chrome"/>
    <suite-files>
        <suite-file path="testng-smoke.xml"/>
    </suite-files>
</suite>
```

## §3 — BaseTest & Thread-Safe Driver

```java
public class BaseTest {
    private static final ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();
    protected WebDriver driver;

    @Parameters({"browser", "env"})
    @BeforeMethod(alwaysRun = true)
    public void setUp(@Optional("chrome") String browser, @Optional("qa") String env) {
        WebDriver d;
        switch (browser.toLowerCase()) {
            case "firefox":
                WebDriverManager.firefoxdriver().setup();
                d = new FirefoxDriver();
                break;
            case "edge":
                WebDriverManager.edgedriver().setup();
                d = new EdgeDriver();
                break;
            default:
                ChromeOptions opts = new ChromeOptions();
                if ("ci".equals(System.getenv("CI"))) {
                    opts.addArguments("--headless=new", "--no-sandbox", "--disable-gpu");
                }
                WebDriverManager.chromedriver().setup();
                d = new ChromeDriver(opts);
        }
        d.manage().window().maximize();
        d.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        d.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
        driverThread.set(d);
        driver = d;

        String baseUrl = ConfigReader.get(env + ".base.url");
        driver.get(baseUrl);
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            captureScreenshot(result.getName());
        }
        WebDriver d = driverThread.get();
        if (d != null) { d.quit(); driverThread.remove(); }
    }

    public WebDriver getDriver() { return driverThread.get(); }

    private void captureScreenshot(String testName) {
        try {
            File src = ((TakesScreenshot) getDriver()).getScreenshotAs(OutputType.FILE);
            String path = "screenshots/" + testName + "_" + System.currentTimeMillis() + ".png";
            FileUtils.copyFile(src, new File(path));
            Allure.addAttachment("Screenshot", new FileInputStream(new File(path)));
        } catch (Exception e) { /* log */ }
    }
}
```

### Configuration Reader

```java
public class ConfigReader {
    private static final Properties props = new Properties();

    static {
        String env = System.getProperty("env", "qa");
        try {
            props.load(ConfigReader.class.getResourceAsStream("/config-" + env + ".properties"));
        } catch (IOException e) { throw new RuntimeException(e); }
    }

    public static String get(String key) {
        return System.getProperty(key, props.getProperty(key));
    }
}
```

## §4 — Data Providers (Advanced)

```java
// Parallel DataProvider
@DataProvider(name = "loginData", parallel = true)
public Object[][] loginData() {
    return new Object[][] {
        {"admin@test.com", "admin123", true},
        {"user@test.com", "wrong", false},
        {"", "", false},
        {"sql@inject.com", "' OR 1=1 --", false},
    };
}

@Test(dataProvider = "loginData", groups = "smoke")
public void testLogin(String email, String password, boolean expected) {
    assertEquals(loginService.authenticate(email, password), expected);
}

// Excel DataProvider
@DataProvider(name = "excelData")
public Object[][] excelData() throws Exception {
    return ExcelReader.getData("testdata/users.xlsx", "Sheet1");
}

// JSON DataProvider
@DataProvider(name = "jsonData")
public Object[][] jsonData() throws Exception {
    List<Map<String, String>> data = JsonReader.readArray("testdata/cases.json");
    return data.stream().map(m -> new Object[]{m}).toArray(Object[][]::new);
}

// CSV DataProvider with Iterator (memory efficient)
@DataProvider(name = "csvData")
public Iterator<Object[]> csvData() throws Exception {
    CSVReader reader = new CSVReader(new FileReader("testdata/large-dataset.csv"));
    return reader.readAll().stream()
        .skip(1)  // skip header
        .map(row -> (Object[]) row)
        .iterator();
}

// Cross-class DataProvider
public class TestDataProviders {
    @DataProvider(name = "sharedData")
    public static Object[][] sharedData() {
        return new Object[][] { {"data1"}, {"data2"} };
    }
}

@Test(dataProvider = "sharedData", dataProviderClass = TestDataProviders.class)
public void testWithSharedData(String data) { /* ... */ }
```

## §5 — Factory Pattern

```java
public class DynamicLoginTest extends BaseTest {
    private String browser;
    private String resolution;

    @Factory(dataProvider = "browserMatrix")
    public DynamicLoginTest(String browser, String resolution) {
        this.browser = browser;
        this.resolution = resolution;
    }

    @DataProvider(name = "browserMatrix")
    public static Object[][] browserMatrix() {
        return new Object[][] {
            {"chrome", "1920x1080"},
            {"chrome", "1366x768"},
            {"firefox", "1920x1080"},
            {"edge", "1920x1080"},
        };
    }

    @Test(groups = "crossBrowser")
    public void testLoginRendering() {
        // each Factory instance runs as independent test
        driver.manage().window().setSize(parseDimension(resolution));
        // ... assert login page renders correctly
    }
}
```

## §6 — Listeners (Production Suite)

```java
// Retry Analyzer — configurable
public class RetryAnalyzer implements IRetryAnalyzer {
    private int count = 0;
    private static final int MAX = Integer.parseInt(
        System.getProperty("retry.max", "2"));

    @Override
    public boolean retry(ITestResult result) {
        if (count < MAX) { count++; return true; }
        return false;
    }
}

// Global retry via transformer
public class RetryListener implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation annotation, Class testClass,
                          Constructor ctor, Method method) {
        if (annotation.getRetryAnalyzerClass() == null) {
            annotation.setRetryAnalyzer(RetryAnalyzer.class);
        }
    }
}

// Screenshot + Allure on failure
public class ScreenshotListener implements ITestListener {
    @Override
    public void onTestFailure(ITestResult result) {
        Object instance = result.getInstance();
        if (instance instanceof BaseTest) {
            WebDriver driver = ((BaseTest) instance).getDriver();
            if (driver != null) {
                byte[] screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
                Allure.addAttachment("Failure Screenshot", "image/png",
                    new ByteArrayInputStream(screenshot), "png");
            }
        }
    }

    @Override
    public void onTestStart(ITestResult result) {
        Log.info("Starting: " + result.getMethod().getMethodName());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        Log.info("Passed: " + result.getMethod().getMethodName() +
                 " in " + (result.getEndMillis() - result.getStartMillis()) + "ms");
    }
}

// Execution timing listener
public class TimingListener implements ISuiteListener {
    @Override
    public void onStart(ISuite suite) {
        suite.setAttribute("startTime", System.currentTimeMillis());
    }

    @Override
    public void onFinish(ISuite suite) {
        long start = (long) suite.getAttribute("startTime");
        long duration = System.currentTimeMillis() - start;
        Log.info("Suite completed in " + duration + "ms");
        Log.info("Passed: " + suite.getAllMethods().stream()
            .filter(m -> m.getCurrentInvocationCount() > 0).count());
    }
}
```

## §7 — Soft Assertions & Dependencies

```java
// Soft assertions — report ALL failures
@Test
public void testUserProfile() {
    SoftAssert soft = new SoftAssert();
    User user = userService.getUser(1);
    soft.assertEquals(user.getName(), "Alice", "Name mismatch");
    soft.assertEquals(user.getEmail(), "alice@test.com", "Email mismatch");
    soft.assertTrue(user.isActive(), "User should be active");
    soft.assertNotNull(user.getCreatedAt(), "Created date missing");
    soft.assertAll();
}

// Method dependencies
@Test(priority = 1, groups = "crud")
public void createUser() { userId = userService.create(testUser); }

@Test(priority = 2, dependsOnMethods = "createUser", groups = "crud")
public void verifyUser() { assertNotNull(userService.get(userId)); }

@Test(priority = 3, dependsOnMethods = "createUser", groups = "crud")
public void deleteUser() { userService.delete(userId); }

// Group dependencies
@Test(groups = "setup")
public void seedDatabase() { /* ... */ }

@Test(dependsOnGroups = "setup", groups = "regression")
public void testWithSeededData() { /* ... */ }
```

## §8 — Page Object Integration

```java
public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

    @FindBy(id = "email") private WebElement emailInput;
    @FindBy(id = "password") private WebElement passwordInput;
    @FindBy(css = "[data-testid='login-btn']") private WebElement loginButton;
    @FindBy(css = ".error-message") private WebElement errorMessage;

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(driver, this);
    }

    public DashboardPage loginAs(String email, String password) {
        emailInput.clear();
        emailInput.sendKeys(email);
        passwordInput.clear();
        passwordInput.sendKeys(password);
        loginButton.click();
        return new DashboardPage(driver);
    }

    public String getErrorMessage() {
        wait.until(ExpectedConditions.visibilityOf(errorMessage));
        return errorMessage.getText();
    }
}

// Test using Page Object
public class LoginTest extends BaseTest {
    @Test(groups = "smoke", dataProvider = "validLogins")
    public void testSuccessfulLogin(String email, String password) {
        LoginPage loginPage = new LoginPage(driver);
        DashboardPage dashboard = loginPage.loginAs(email, password);
        assertTrue(dashboard.isLoaded(), "Dashboard should load after login");
    }
}
```

## §9 — Parallel Execution Strategies

```xml
<!-- Method-level: fastest, requires thread-safe tests -->
<suite parallel="methods" thread-count="8">

<!-- Class-level: each class in its own thread (recommended) -->
<suite parallel="classes" thread-count="4">

<!-- Test-level: each <test> block in its own thread -->
<suite parallel="tests" thread-count="3">

<!-- Instance-level: with @Factory -->
<suite parallel="instances" thread-count="4">

<!-- Mixed: different parallelism per test block -->
<suite parallel="tests" thread-count="3">
    <test name="UITests" parallel="classes" thread-count="2">
        <classes>...</classes>
    </test>
    <test name="APITests" parallel="methods" thread-count="8">
        <classes>...</classes>
    </test>
</suite>
```

### Thread-Safe Guidelines

```java
// Thread-safe: Use ThreadLocal for shared resources
private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
private static final ThreadLocal<SoftAssert> softAssert = new ThreadLocal<>();

@BeforeMethod
public void init() {
    driver.set(new ChromeDriver());
    softAssert.set(new SoftAssert());
}
```

## §10 — Reporting Integration

```java
// Allure annotations
@Epic("User Management")
@Feature("Login")
@Story("Successful Login")
@Severity(SeverityLevel.CRITICAL)
@Description("Verifies that valid credentials allow login")
@Test(groups = "smoke")
public void testSuccessfulLogin() {
    Allure.step("Navigate to login page", () -> driver.get(baseUrl + "/login"));
    Allure.step("Enter credentials", () -> {
        loginPage.enterEmail("admin@test.com");
        loginPage.enterPassword("admin123");
    });
    Allure.step("Submit and verify", () -> {
        loginPage.clickLogin();
        assertTrue(dashboard.isLoaded());
    });
}

// ExtentReports listener
public class ExtentListener implements ITestListener {
    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();

    @Override
    public void onStart(ITestContext context) {
        extent = new ExtentReports();
        extent.attachReporter(new ExtentSparkReporter("reports/extent-report.html"));
    }

    @Override
    public void onTestStart(ITestResult result) {
        test.set(extent.createTest(result.getMethod().getMethodName()));
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        test.get().pass("Passed");
    }

    @Override
    public void onTestFailure(ITestResult result) {
        test.get().fail(result.getThrowable());
    }

    @Override
    public void onFinish(ITestContext context) { extent.flush(); }
}
```

## §11 — CI/CD Integration

```yaml
# GitHub Actions
name: TestNG Suite
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        suite: [testng-smoke.xml, testng-regression.xml]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 17 }
      - run: mvn test -DsuiteXmlFile=${{ matrix.suite }} -Denv=ci
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.suite }}
          path: |
            test-output/
            target/surefire-reports/
            allure-results/
```

```yaml
# Jenkins Pipeline
pipeline {
    agent any
    parameters {
        choice(name: 'SUITE', choices: ['smoke', 'regression', 'full'], description: 'Test suite')
        choice(name: 'ENV', choices: ['qa', 'staging', 'prod'], description: 'Environment')
    }
    stages {
        stage('Test') {
            steps {
                sh "mvn test -DsuiteXmlFile=testng-${params.SUITE}.xml -Denv=${params.ENV}"
            }
            post {
                always {
                    testNG(reportFilenamePattern: '**/testng-results.xml')
                    allure includeProperties: false, jdk: '', results: [[path: 'allure-results']]
                }
            }
        }
    }
}
```

## §12 — Debugging Quick-Reference

| Problem | Cause | Fix |
|---------|-------|-----|
| Test not found/running | Missing `@Test` or wrong suite XML path | Verify annotations, check `<classes>` in XML |
| `NullPointerException` in parallel | Shared mutable state across threads | Use `ThreadLocal` for driver and data |
| `DependencyException` | Dependent method failed or not found | Check method name spelling, ensure dependency runs first |
| DataProvider returns 0 rows | File path wrong or empty dataset | Verify file path relative to resources, check data format |
| `@BeforeMethod` not running | Missing `alwaysRun = true` | Add `alwaysRun = true` for grouped tests |
| Retry runs infinitely | Counter not per-instance | Ensure `count` resets per test (instance variable) |
| Groups not filtering | Wrong group name or XML config | Check `<include>` names match `@Test(groups = ...)` |
| Listener not triggered | Not registered in XML or `@Listeners` | Add to `<listeners>` in testng.xml |
| Flaky parallel tests | Race conditions on shared resources | Use ThreadLocal, avoid static mutable state |
| Screenshots missing | Driver already quit in `@AfterMethod` | Capture screenshot before `driver.quit()` |
| `@Factory` tests not independent | Shared state between factory instances | Each Factory instance should be fully independent |
| Allure report empty | Missing `allure-testng` dependency or listener | Add dependency + register `AllureTestNg` listener |

## §13 — Best Practices Checklist

- ✅ Use `ThreadLocal<WebDriver>` for parallel-safe driver management
- ✅ Use `@DataProvider(parallel = true)` for data-driven parallelism
- ✅ Use `alwaysRun = true` on setup/teardown with groups
- ✅ Use `SoftAssert` when multiple independent checks needed
- ✅ Use groups (`smoke`, `regression`, `wip`) for selective execution
- ✅ Use listeners (not base classes) for cross-cutting concerns
- ✅ Use `@Factory` for cross-browser / cross-config matrix tests
- ✅ Use Allure or ExtentReports for rich HTML reports
- ✅ Configure retry analyzer with env-configurable max count
- ✅ Keep `dependsOnMethods` minimal — prefer independent tests
- ✅ Use `@Parameters` for suite-level config (browser, env)
- ✅ Use `ConfigReader` pattern for multi-environment properties
- ✅ Store screenshots and logs as Allure attachments
- ✅ Structure: `tests/`, `pages/`, `listeners/`, `utils/`, `testdata/`

````
