A TestNG suite is only useful if it runs reliably for everyone and in CI. This guide covers running TestNG from the Maven command line, wiring it into Jenkins and GitHub Actions, and the best practices and common mistakes that decide whether a suite stays trustworthy as it grows.
Running TestNG from the Command Line with Maven
# Run default testng.xml
mvn clean test
# Run specific testng.xml
mvn clean test -DsuiteXmlFile=testng_smoke.xml
# Override parameters
mvn clean test -Dbrowser=firefox -Durl=https://production.myapp.com
# Run specific groups
mvn clean test -Dgroups=smoke
# Run one test method
mvn clean test -Dtest=LoginTest#testValidLogin
# Run in parallel with custom thread count
mvn clean test -DthreadCount=5 -Dparallel=methods
Properties like -DsuiteXmlFile and -Dbrowser only work if your pom.xml or code reads them. For example, set <suiteXmlFile>${suiteXmlFile}</suiteXmlFile> in the Surefire configuration with a default value in <properties>, and read the browser with System.getProperty("browser", "chrome"). When a suite file is used, the parallel settings inside testng.xml take priority over command-line ones.
Jenkins Integration
// Jenkinsfile — declarative pipeline
pipeline {
agent any
tools { maven 'Maven_3.9'; jdk 'JDK_17' }
parameters {
choice(name: 'BROWSER', choices: ['chrome','firefox','edge'], description: 'Browser')
choice(name: 'SUITE', choices: ['testng.xml','testng_smoke.xml'], description: 'Suite')
string(name: 'BASE_URL', defaultValue: 'https://staging.myapp.com', description: 'URL')
}
stages {
stage('Checkout') {
steps { git branch: 'main', url: 'https://github.com/org/project.git' }
}
stage('Run Tests') {
steps {
sh "mvn clean test -DsuiteXmlFile=${params.SUITE} -Dbrowser=${params.BROWSER} -Durl=${params.BASE_URL}"
}
}
}
post {
always {
junit 'target/surefire-reports/*.xml' // or the TestNG Results plugin
publishHTML([reportDir: 'reports', reportFiles: 'TestReport.html', reportName: 'Extent Report'])
}
failure { emailext to: 'team@co.com', subject: 'Tests Failed', body: '${BUILD_URL}' }
}
}
The post { always { … } } block publishes results and the Extent report even when tests fail, which is exactly when you need them.
GitHub Actions Integration
# .github/workflows/testng-suite.yml
name: TestNG Automation
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 1 * * *' # nightly at 1 AM
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox] # run on both browsers in parallel
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- uses: browser-actions/setup-chrome@latest
- uses: browser-actions/setup-firefox@latest
- name: Run TestNG Suite
run: mvn clean test -DsuiteXmlFile=testng.xml -Dbrowser=${{ matrix.browser }}
- name: Upload Test Reports
uses: actions/upload-artifact@v4
if: always()
with:
name: test-reports-${{ matrix.browser }}
path: |
reports/
target/surefire-reports/
- name: Publish TestNG Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: target/surefire-reports/TEST-*.xml
Best Practices
Test Design
- One test method = one behaviour — do not test multiple features in one @Test
- Use @Test(description) for every test — improves reports and traceability
- Name tests descriptively: testLogin_validCredentials_shouldRedirectToDashboard
- Every test must be independent — never rely on another test's side effects
- Use SoftAssert when verifying multiple properties of the same page/object
- Always add a failure message to assertions: Assert.assertEquals(a, e, 'message')
Framework Design
- Use ThreadLocal<WebDriver> for all parallel execution — never static WebDriver
- Call driver.quit() and then ThreadLocal.remove() in @AfterMethod — prevents ThreadLocal memory leaks
- Use @BeforeMethod(alwaysRun=true) for setup — runs even if previous test failed
- Use @AfterMethod(alwaysRun=true) for teardown — always close browser
- Store all locators in Page Object classes — never in test classes
- Store all configuration in config.properties — never hardcode URLs/credentials
Common Mistakes and How to Fix Them
| Mistake | Problem | Correct Approach |
|---|---|---|
| Static WebDriver field | Parallel tests share one browser — chaos | ThreadLocal<WebDriver> in DriverFactory |
| Not calling softAssert.assertAll() | Test always passes — silently swallows failures | ALWAYS call assertAll() at end of test |
| Mixing @BeforeMethod and @BeforeClass wrongly | Driver not ready / browser launched too many times | BeforeMethod for per-test setup, BeforeClass for expensive once-only setup |
| Hard Assert for multiple checks | First failure hides all others | SoftAssert for validating multiple properties |
| Test order dependency without dependsOnMethods | Tests fail when run alone or in different order | Use dependsOnMethods or make tests truly independent |
| forgetting alwaysRun=true on teardown | @AfterMethod skipped if test throws unexpected exception | @AfterMethod(alwaysRun=true) — always clean up |
| Printing sensitive data in logs | Credentials appear in CI logs | Use config files, mask in CI environment variables |
| No groups on tests | Cannot run smoke/regression subsets | Always add at minimum one group to each test |
FAQs
How do I run a specific testng.xml with Maven?
Reference a property in the Surefire configuration, for example <suiteXmlFile>${suiteXmlFile}</suiteXmlFile>, then run mvn test -DsuiteXmlFile=testng_smoke.xml.
How do you publish TestNG results in Jenkins?
Surefire writes JUnit-format XML to target/surefire-reports; publish it with the junit step (or the TestNG Results plugin) in a post { always { } } block, and publish HTML reports with the HTML Publisher plugin.
Why use alwaysRun = true on teardown methods?
So cleanup such as quitting the browser still runs when a test or configuration method fails, preventing leftover browsers and cascading failures.
Should every TestNG test belong to a group?
Yes, at least one (smoke, regression and so on), so pipelines can run fast subsets on every commit and the full suite on a schedule.