How to Run Database Tests with Selenium and TestNG
Combine Selenium, TestNG, and JDBC to verify browser workflows and the database state they create, with isolation, cleanup, and troubleshooting guidance.
Use Selenium for the browser, TestNG for test lifecycle and assertions, and JDBC for database setup and verification. A reliable test creates unique data, exercises the user flow in a WebDriver session, checks the persisted state with a narrowly scoped SQL query, and cleans up even when an assertion fails.
This guide shows a maintainable Java structure, Maven setup, complete example code, configuration choices, isolation rules, parallel execution guidance, troubleshooting, and an API option for workflows that need screenshots.
1. How the pieces fit together
| Component | Responsibility |
|---|---|
| Selenium WebDriver | Starts and controls a browser, navigates to pages, fills forms, clicks controls, and reads visible results. |
| TestNG | Discovers tests, runs setup and teardown methods, groups tests, controls execution, and provides assertions. |
| JDBC | Connects Java to a database, executes prepared queries or updates, and reads result sets. |
Selenium does not replace a test runner, and TestNG does not provide database access. Keeping those roles separate makes a failure easier to diagnose: a browser assertion points to the user flow, while a JDBC assertion points to persistence.
2. Project setup
Install a JDK, the browser under test, the matching WebDriver implementation, the Selenium Java binding, TestNG, and the JDBC driver for your database. TestNG 7.6.0 and newer requires JDK 11 or newer; verify current versions on the official project pages before pinning dependencies.
Maven dependencies
<properties>
<maven.compiler.release>17</maven.compiler.release>
<selenium.version>4.x.x</selenium.version>
<testng.version>7.9.0</testng.version>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>your-jdbc-driver</artifactId>
<version>YOUR_DRIVER_VERSION</version>
<scope>test</scope>
</dependency>
</dependencies>
Replace the JDBC coordinates with the driver supplied by your database vendor. Keep connection strings, usernames, and passwords outside source control.
3. A complete Selenium, TestNG, and JDBC test
The following example uses TestNG parameters for the application and database connection. Replace the URL paths, locators, table name, and SQL with your schema.
TestNG suite parameters
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Database-backed UI tests">
<parameter name="appBaseUrl" value="http://localhost:8080"/>
<parameter name="jdbcUrl" value="jdbc:yourdb://localhost:5432/app"/>
<parameter name="dbUser" value="test_user"/>
<parameter name="dbPassword" value="change-me"/>
<test name="Profile persistence">
<classes>
<class name="example.ProfilePersistenceTest"/>
</classes>
</test>
</suite>
Use environment variables or your build system’s secret store for real credentials. The XML example is intentionally simple and should not contain production secrets.
Java test class
package example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.util.UUID;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
public class ProfilePersistenceTest {
private WebDriver driver;
private String appBaseUrl;
private String jdbcUrl;
private String dbUser;
private String dbPassword;
private String email;
@BeforeMethod
@Parameters({"appBaseUrl", "jdbcUrl", "dbUser", "dbPassword"})
public void setUp(String appBaseUrl, String jdbcUrl, String dbUser, String dbPassword) {
this.appBaseUrl = appBaseUrl;
this.jdbcUrl = jdbcUrl;
this.dbUser = dbUser;
this.dbPassword = dbPassword;
this.email = "test-" + UUID.randomUUID() + "@example.invalid";
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
}
@Test
public void savedProfileAppearsInDatabase() throws SQLException {
driver.get(appBaseUrl + "/signup");
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys("A-test-password-123!");
driver.findElement(By.cssSelector("button[type='submit']")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.urlContains("/welcome"));
Assert.assertTrue(driver.getPageSource().contains("Account created"));
String sql = "select email from users where email = ?";
try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPassword);
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, email);
try (ResultSet results = statement.executeQuery()) {
Assert.assertTrue(results.next(), "Expected a row for " + email);
Assert.assertEquals(results.getString("email"), email);
}
}
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
if (driver != null) {
driver.quit();
}
// Delete only this test's record through an application cleanup path
// or a parameterized JDBC DELETE, if the test environment permits it.
}
}
This is a template: the application URL, locators, table, columns, and expected text are application-specific. The important properties are a unique identifier, a prepared statement, scoped resources, and teardown that runs after failures.
4. Prepare data without making tests flaky
- Create or identify data owned by the current test. A UUID-based email or external ID prevents collisions.
- Prefer an application API or fixture loader when that is the supported way to create valid domain data.
- Use JDBC setup when direct database preparation is necessary, but keep SQL close to the test fixture code.
- Record the identifier so cleanup can target only rows created by this test.
Never assert against a generic row such as “the newest user.” Another test, a background job, or an existing fixture can change that row between the UI action and the query.
5. JDBC practices that matter in tests
Use prepared statements
String sql = "update orders set status = ? where external_id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "PAID");
statement.setString(2, externalId);
int changed = statement.executeUpdate();
Assert.assertEquals(changed, 1);
}
Bind every variable value with the matching setter. Do not concatenate test input into SQL.
Close resources automatically
Use try-with-resources for Connection, PreparedStatement, and ResultSet. Resources close when the block ends, including when an exception is thrown.
Choose a connection strategy
| Strategy | Use when |
|---|---|
DriverManager |
A small example or isolated test needs one straightforward connection. |
DataSource |
The suite has many tests and benefits from centralized configuration or pooling. Oracle’s JDBC tutorial presents it as the preferred approach. |
6. TestNG lifecycle and configuration choices
| Annotation | Typical use |
|---|---|
@BeforeSuite / @AfterSuite |
One-time environment preparation or final reporting. |
@BeforeClass / @AfterClass |
Setup shared by tests in one class when state is controlled. |
@BeforeMethod / @AfterMethod |
Fresh browser and data for every test; usually the safest default for UI tests. |
TestNG’s @Parameters injects values from testng.xml; @Optional can provide a default. Use method scope when isolation matters more than setup time. Use class or suite scope only when shared browser or database state cannot leak between tests.
7. Assertions that explain failures
Assert the user-visible result first, then persistence. Include the test identifier in failure messages. Query only the columns needed for the requirement and check cardinality when duplicates would indicate a defect.
String sql = "select status, total_cents from orders where external_id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, externalId);
try (ResultSet rs = statement.executeQuery()) {
Assert.assertTrue(rs.next(), "No order found for " + externalId);
Assert.assertEquals(rs.getString("status"), "PAID");
Assert.assertEquals(rs.getInt("total_cents"), 2500);
Assert.assertFalse(rs.next(), "More than one order found for " + externalId);
}
}
8. Parallel execution and isolation
Start with serial execution. Once tests are stable, TestNG can run methods or classes in parallel, but every test must have its own WebDriver session, unique records, and safe database transactions.
- Do not store a mutable WebDriver in a static field.
- Do not reuse a shared email, account, or order identifier.
- Make cleanup idempotent so a retry does not fail because the row is already gone.
- Watch for database locks, unique constraints, and background jobs that process shared tables.
- Keep browser and connection lifetimes aligned with the TestNG scope that owns them.
Parallel execution is a test-design decision, not only a TestNG setting. Enable it after proving isolation with serial runs.
9. Waiting, transactions, and eventual consistency
Use explicit Selenium waits for a page condition, such as a URL change or visible success message. Avoid arbitrary sleeps unless the application exposes no observable condition.
If persistence is asynchronous, the UI assertion may pass before the row is committed. In that case, poll a narrowly scoped JDBC query for a bounded period, then fail with the identifier and last observed state. Keep transactions explicit: commit setup data before the browser needs it, and roll back fixture changes when the database supports that pattern.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser does not start | Missing browser, incompatible driver, or unsupported runtime. | Install the browser and matching WebDriver; check Selenium and JDK versions. |
ElementNotInteractableException |
The element is hidden, disabled, or not ready. | Wait for visibility or clickability and verify the locator. |
| Timeout waiting for page state | Wrong URL, locator, or application error. | Capture the current URL and page source; verify the selector and application logs. |
| JDBC connection refused | Database unavailable, wrong host or port, or network policy. | Check the database process, connection string, credentials, and test runner network. |
| Authentication failure | Incorrect or expired secret. | Load credentials from the runtime secret store and validate the selected environment. |
| SQL syntax or table error | Schema differs from the test SQL. | Run the query against the same schema and migration version used by the test. |
| Expected row is missing | Asynchronous write, rollback, wrong tenant, or test data collision. | Check commit behavior, poll with a timeout when appropriate, and use a unique identifier. |
| Tests pass alone but fail together | Shared browser, fixture, or database state. | Move setup to method scope, isolate identifiers, and disable parallelism while diagnosing. |
| Cleanup hides the original failure | Teardown throws another exception. | Use alwaysRun=true, make cleanup defensive, and preserve the original assertion message. |
11. Performance, reliability, and cost
- Performance: Reuse expensive suite setup only when state is immutable or reset between tests. A
DataSourcecan centralize connection management for larger suites. - Reliability: Prefer deterministic locators, explicit waits, unique test data, prepared statements, and guaranteed teardown.
- Database load: Select only required columns and avoid broad scans. Parallel browser tests can create concurrent writes and locks.
- Diagnostics: Record the test identifier, browser URL, SQL operation name, and timing. Never log passwords or full connection secrets.
- Cost: Selenium, TestNG, and JDBC are libraries; infrastructure cost comes from browsers, CI workers, and the database environment. Keep a small smoke suite for every change and run broader cross-browser coverage on an appropriate schedule.
12. Or skip the browser setup
If the test only needs a clean visual capture of a page or result, ScreenshotNeo provides a single request to return PNG, JPEG, WebP, or PDF. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page and element captures, device or custom viewports, dark mode, retina scale, custom CSS and JavaScript, waits, headers, cookies, user agents, blocking rules, caching, PDFs, bulk capture, signed links, async jobs, webhooks, and an MCP server with take_screenshot, get_page_info, and capture_pdf tools.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
13. FAQ
Should database setup happen through JDBC or the application API?
Use the application API when it is the supported path for valid domain state. Use JDBC when direct fixture control is necessary and the test environment allows it.
Should every test create a new browser?
A fresh browser per test gives the clearest isolation. Sharing a browser can reduce setup time but requires careful reset of cookies, storage, navigation, and user state.
Can I run TestNG tests in parallel?
Yes, after each test owns separate data and a separate WebDriver session and database contention is understood.
What should a persistence assertion verify?
Verify the specific row and columns required by the behavior, using the unique identifier created by that test.
When is a screenshot API useful in this workflow?
Use one when visual evidence or page capture is needed without maintaining browser-driver setup. ScreenshotNeo can also be called by AI agents through its MCP server.
14. Practical checklist
- Pin compatible JDK, Selenium, TestNG, browser, driver, and JDBC versions.
- Keep secrets outside source code.
- Generate a unique identifier for every test-owned record.
- Use explicit waits and deterministic locators.
- Use
PreparedStatementand try-with-resources. - Assert the UI result and then the narrowly scoped database state.
- Clean up with
@AfterMethod(alwaysRun = true). - Run serially before enabling TestNG parallel modes.
- Capture identifiers and useful diagnostics without logging secrets.


