Use Selenium WebDriver to exercise the application through its browser interface, TestNG to organize and run the test, and JDBC to prepare or verify database state. Give each test its own data, check both what the user sees and what the database stores when persistence matters, and clean up the browser and test data even when an assertion fails.
What Selenium, TestNG, and JDBC each do
- Selenium WebDriver drives a browser: it navigates to pages and interacts with the application as a user would.
- TestNG runs Java tests, provides assertions, and supplies lifecycle hooks for setup and cleanup.
- JDBC connects Java code to a database, runs queries or updates, and returns results.
These tools cover different parts of a test. A JDBC assertion alone does not prove a user can complete a workflow; a browser assertion alone may not prove that data persisted correctly. Combine them when the requirement is both a usable flow and a correct stored result.
Set up the project and environment
- Install Java, the browser, and Selenium’s Java binding. Selenium’s setup guidance calls for the language binding, the browser under test, and its matching WebDriver implementation.
- Add TestNG as a test dependency. Its official project page lists version 7.9.0 and states that TestNG 7.6.0 and newer requires JDK 11 or higher. Releases can change, so check the project page when selecting a version rather than treating that version as a permanent recommendation.
- Add the JDBC driver for your database. The driver depends on the database product; the JDBC tutorial cited here is written for JDK 8 and warns that some examples may not reflect newer releases.
- Provide environment-specific settings at runtime. Keep the application URL and database connection details out of committed test source. TestNG supports values passed through
testng.xmlwith@Parametersand defaults with@Optional. Handle credentials through your build or runtime secret-management setup; parameter injection is not itself a secret store.
Use a test database or otherwise ensure that test-created rows cannot affect production or real users. The precise database, schema, and environment safeguards depend on the application.
Choose where to create and verify test data
Prepare data through the application or JDBC
Create a record through an application API when the test needs realistic application-level setup, or use JDBC when direct database setup is appropriate for the test environment. In either case, assign the record a unique identifier, such as an email address containing a UUID. Do not have tests repeatedly overwrite a shared row: parallel runs or retries can collide and make results unreliable.
#1 Best Overall
Verify persistence with a narrow query
After submitting the browser flow, query only the row owned by the test, using its unique identifier in a parameterized statement. Keep the visible UI assertion and database assertion as distinct checks. That makes it easier to tell whether the problem is a failed browser interaction, a missing visible result, or unexpected persistence.
A practical TestNG flow
- Prepare: create or identify unique test data and retain its identifier for assertions and cleanup.
- Open a browser: create a WebDriver session and navigate to the application page under test.
- Exercise the user journey: locate elements using selectors appropriate to your application, enter the test data, and submit the form.
- Assert the user-visible result: check the confirmation or resulting state that matters to the user.
- Assert database state if required: use JDBC to select the test-owned row and check the expected stored values.
- Clean up: remove test-owned data through an application cleanup path or JDBC, and quit the browser whether the test passes or fails.
TestNG’s @BeforeMethod and @AfterMethod are suitable lifecycle hooks when each test needs its own state. @BeforeClass/@AfterClass or @BeforeSuite/@AfterSuite can place work at a broader scope, but sharing mutable browser or data state requires care.
Rank #2
Java example: browser flow and JDBC assertion
The following example shows the structure of one TestNG test. It assumes the project provides a configured DataSource, an appBaseUrl, an application signup page with the indicated locators, and an assertion library. Replace the URL path, selectors, schema, and assertion imports with those of your application; no universal signup page or database schema exists.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
import javax.sql.DataSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
public class SignupDatabaseTest {
private final DataSource dataSource = TestConfig.dataSource();
private final String appBaseUrl = TestConfig.appBaseUrl();
@Test
public void savedProfileAppearsInDatabase() throws SQLException {
String email = "test-" + UUID.randomUUID() + "@example.invalid";
WebDriver driver = new ChromeDriver();
try {
driver.get(appBaseUrl + "/signup");
driver.findElement(By.name("email")).sendKeys(email);
driver.findElement(By.cssSelector("button[type='submit']")).click();
// Replace with an assertion against the application's visible result.
assertTrue(driver.getPageSource().contains("Account created"));
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"select email from users where email = ?")) {
statement.setString(1, email);
try (ResultSet results = statement.executeQuery()) {
assertTrue(results.next(), "Expected test-owned user row");
assertEquals(results.getString("email"), email);
}
}
} finally {
driver.quit();
// Delete this test's record through an application cleanup path or JDBC.
}
}
}
TestConfig is deliberately application-owned configuration, not a Selenium or TestNG class. Supply it from your project so URLs and credentials are not embedded in test logic. The cleanup comment must be implemented for a repeatable test; the correct deletion method depends on application constraints and foreign-key relationships.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #3
The visible-result assertion above is only an example of where to assert. A page-source substring can be brittle; prefer a condition tied to the actual confirmation element in your application. Configure a wait if the result appears asynchronously.
Use JDBC safely
- Bind input values: use
PreparedStatementplaceholders and the matching setter, such assetString, rather than concatenating test input into SQL. This keeps variable values separate from SQL syntax. - Close resources: use try-with-resources for
Connection,PreparedStatement(or another statement), andResultSet. They close when the block exits, including when an exception is thrown. - Keep queries scoped: select by the unique identifier generated for the test; avoid broad assertions that can accidentally match another test’s data.
- Select connection management deliberately: Oracle’s Java tutorial prefers
DataSourceand usesDriverManagerfor simpler examples. Choose the connection approach that fits your application and runtime rather than creating ad hoc connections without a lifecycle plan.
Choose lifecycle scope and execution mode
Method, class, or suite setup
Method-level setup is a natural starting point when each test requires isolated browser and database state. Class- or suite-level setup can avoid repeating expensive initialization, but should not make tests depend on mutable state left behind by another test. Put cleanup at the scope that owns the resource and make it run on failure as well as success.
Rank #4
Serial first; parallel after isolation
TestNG supports thread pools and parallel test modes. Begin with serial execution to establish stable behavior. Before enabling concurrency, ensure every test has separate data, a properly scoped WebDriver session, and no writes to shared records that can conflict. Database locking and concurrency safety depend on the database and test design, so framework parallelism alone does not make a test safe to run concurrently.
Common failures and how to address them
- Browser session cannot start: check that the browser is installed and that the WebDriver implementation is available and compatible with the browser under test.
- The test fails to compile or TestNG cannot run: check the TestNG dependency and Java runtime pairing. TestNG 7.6.0 and newer requires JDK 11 or higher.
- JDBC connection fails: verify that the selected database’s JDBC driver is on the test runtime classpath and that the runtime connection settings point to the intended test database.
- The query returns no row: distinguish an application-flow failure from a timing issue or an incorrect query. First check the UI result, then confirm the exact unique identifier submitted, the database/schema being queried, and whether the flow completed before the query ran.
- A test passes alone but fails in a suite: look for shared records, state left by earlier tests, or cleanup that did not run. Give each run unique data and make teardown reliable.
- Parallel runs intermittently fail: return to serial execution, isolate data and browser sessions, then reintroduce concurrency only after shared writes and resource ownership are controlled.
- SQL errors occur with values containing punctuation: replace SQL string concatenation with a prepared statement and bind values using the appropriate setter.
Or skip the browser setup: capture a page with ScreenshotNeo
For a screenshot of a page rather than an end-to-end database assertion, ScreenshotNeo offers a one-request screenshot API and an MCP server for AI agents. It is not a substitute for Selenium interaction or JDBC persistence checks. Its capture can accept cookie/consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be disabled. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. See ScreenshotNeo and the API documentation.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchescurl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Replace YOUR_API_KEY with your key and the target URL with the page to capture. The API can return PNG, JPEG, WebP, or PDF. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
Best Value
Plan for stable, useful tests
- Keep browser actions, visible assertions, and persistence checks easy to distinguish.
- Use unique test-owned data and a cleanup path that survives assertion failures.
- Use prepared statements and automatic JDBC resource closing.
- Start serial; add parallel execution only after data and browser isolation are established.
- Recheck official dependency and runtime requirements when choosing versions, because releases change.
Frequently Asked Questions
Can Selenium verify a database record by itself?
No. Selenium drives the browser; use JDBC or another database-facing mechanism for a direct persistence assertion.
Should every Selenium test query the database?
No. Query it when stored state is part of the requirement. Keep ordinary user-visible behavior checks focused on the browser unless persistence is specifically under test.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

