İçeriğe geç

Java ve JDBC ile Database Otomasyon Testi

Amaç: Uygulamanın veri katmanını (DAO/Repository/ORM-öncesi) otomatik testlerle doğrulamak.
Yaklaşım: Java + JDBC + TestNG/JUnit + (opsiyonel) HikariCP bağlantı havuzu.
Hedefler: CRUD doğrulaması, veri bütünlüğü, transaction yönetimi, performans ve negatif senaryolar.


  • Katmanlar: tests (TestNG/JUnit) → dao (SUT) → jdbc (DB erişimi)
  • Konfigürasyon: application-test.properties (DB URL, kullanıcı, şifre)
  • Bağlantı Havuzu: HikariCP (stabil ve hızlı)
  • Veri Yönetimi: Test fixture’ları (setup/teardown), transaction + rollback yaklaşımı
  • Test İzolasyonu: Her test kendi verisini kurar, test sonunda DB değişmeden kalır
  • Deterministik Testler: ID/primary key değerleri tahmin edilebilir ya da testte yakalanır

<dependencies>
<!-- Test Çatısı -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.0</version>
<scope>test</scope>
</dependency>
<!-- JDBC Sürücüsü (MySQL örneği) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.0.0</version>
</dependency>
<!-- (Opsiyonel) PostgreSQL Sürücüsü -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<!-- (Opsiyonel) Bağlantı Havuzu: HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
<!-- (Opsiyonel) Config okuma -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>

Kaynak ağacı (öneri):

src
├─ main
│ ├─ java
│ │ └─ com.example.db
│ │ ├─ config
│ │ │ └─ DbConfig.java
│ │ ├─ util
│ │ │ └─ DataSourceFactory.java
│ │ └─ dao
│ │ └─ EmployeeDao.java
│ └─ resources
│ └─ application-test.properties
└─ test
└─ java
└─ com.example.db
├─ BaseDbTest.java
└─ EmployeeDaoTest.java

application-test.properties

db.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true
db.username=test_user
db.password=secret
db.pool.maxSize=5

DataSourceFactory.java

package com.example.db.util;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public class DataSourceFactory {
public static DataSource create(String url, String user, String pass, int maxPoolSize) {
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl(url);
cfg.setUsername(user);
cfg.setPassword(pass);
cfg.setMaximumPoolSize(maxPoolSize);
cfg.setAutoCommit(false); // testlerde manuel transaction kontrolü
return new HikariDataSource(cfg);
}
}

DbConfig.java

package com.example.db.config;
import com.example.db.util.DataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
public class DbConfig {
private static DataSource dataSource;
public static DataSource getDataSource() {
if (dataSource == null) {
try {
Properties p = new Properties();
p.load(DbConfig.class.getClassLoader().getResourceAsStream("application-test.properties"));
dataSource = DataSourceFactory.create(
p.getProperty("db.url"),
p.getProperty("db.username"),
p.getProperty("db.password"),
Integer.parseInt(p.getProperty("db.pool.maxSize", "5"))
);
} catch (IOException e) {
throw new RuntimeException("Konfigürasyon yüklenemedi", e);
}
}
return dataSource;
}
}

EmployeeDao.java

package com.example.db.dao;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class EmployeeDao {
private final DataSource ds;
public EmployeeDao(DataSource ds) {
this.ds = ds;
}
public long insert(String name, String job, double salary) throws SQLException {
String sql = "INSERT INTO employees(name, job, salary) VALUES (?, ?, ?)";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setString(2, job);
ps.setDouble(3, salary);
int affected = ps.executeUpdate();
if (affected != 1) throw new SQLException("Beklenen 1 satır etkilenmedi");
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) return rs.getLong(1);
throw new SQLException("ID oluşturulamadı");
}
}
}
public int updateSalary(long id, double salary) throws SQLException {
String sql = "UPDATE employees SET salary=? WHERE id=?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setDouble(1, salary);
ps.setLong(2, id);
return ps.executeUpdate();
}
}
public int delete(long id) throws SQLException {
String sql = "DELETE FROM employees WHERE id=?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, id);
return ps.executeUpdate();
}
}
public List<String> findByJob(String job) throws SQLException {
String sql = "SELECT name FROM employees WHERE job=?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, job);
try (ResultSet rs = ps.executeQuery()) {
List<String> names = new ArrayList<>();
while (rs.next()) names.add(rs.getString("name"));
return names;
}
}
}
}

Not: Test izolasyonu için DAO metodlarında Connection yönetimini doğrudan testlerin kontrol etmesi de tercih edilebilir (ör. tek connection aç, test boyunca aynı transaction’da çalış ve rollback et). Aşağıda bu yaklaşım gösterilmiştir.


6) Test Altyapısı: Transaction + Rollback

Section titled “6) Test Altyapısı: Transaction + Rollback”

BaseDbTest.java

package com.example.db;
import com.example.db.config.DbConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import javax.sql.DataSource;
import java.sql.Connection;
public abstract class BaseDbTest {
protected DataSource ds;
protected Connection txConn;
@BeforeMethod
public void startTransaction() throws Exception {
ds = DbConfig.getDataSource();
txConn = ds.getConnection();
txConn.setAutoCommit(false);
}
@AfterMethod(alwaysRun = true)
public void rollback() throws Exception {
if (txConn != null) {
txConn.rollback(); // her test sonunda DB temiz
txConn.close();
}
}
}

DAO’yu transaction connection ile kullanma (alternatif örnek)

// Test içinde:
try (var ps = txConn.prepareStatement("INSERT INTO employees(name, job, salary) VALUES (?, ?, ?)", java.sql.Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "Ayşe");
ps.setString(2, "QA Engineer");
ps.setDouble(3, 7000);
ps.executeUpdate();
// ...
}
// Diğer sorgular aynı txConn ile yürütülür.
// Test bitince BaseDbTest.rollback() her şeyi geri alır.

EmployeeDaoTest.java

package com.example.db;
import com.example.db.dao.EmployeeDao;
import org.testng.annotations.Test;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.List;
import static org.testng.Assert.*;
public class EmployeeDaoTest extends BaseDbTest {
@Test
public void shouldInsertAndQueryEmployee() throws Exception {
// Arrange: test verisi oluştur
long id;
try (PreparedStatement ps = txConn.prepareStatement(
"INSERT INTO employees(name, job, salary) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "Mehmet");
ps.setString(2, "QA Engineer");
ps.setDouble(3, 6500);
int affected = ps.executeUpdate();
assertEquals(affected, 1);
try (ResultSet keys = ps.getGeneratedKeys()) {
assertTrue(keys.next());
id = keys.getLong(1);
}
}
// Act: DAO ile sorgula
EmployeeDao dao = new EmployeeDao(ds);
List<String> names = dao.findByJob("QA Engineer");
// Assert
assertTrue(names.contains("Mehmet"));
// Cleanup: yok — rollback zaten her şeyi geri alacak
}
@Test
public void shouldUpdateSalary() throws Exception {
long id;
try (PreparedStatement ps = txConn.prepareStatement(
"INSERT INTO employees(name, job, salary) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "Zeynep");
ps.setString(2, "Developer");
ps.setDouble(3, 9000);
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
keys.next();
id = keys.getLong(1);
}
}
int updated = txConn.createStatement()
.executeUpdate("UPDATE employees SET salary=9500 WHERE id=" + id);
assertEquals(updated, 1);
try (ResultSet rs = txConn.createStatement()
.executeQuery("SELECT salary FROM employees WHERE id=" + id)) {
assertTrue(rs.next());
assertEquals(rs.getDouble(1), 9500.0, 0.001);
}
}
}

CSV ile DataProvider (örnek)

import org.testng.annotations.DataProvider;
import java.nio.file.*;
import java.util.*;
public class CsvDataProviders {
@DataProvider(name = "employeeData")
public static Object[][] employeeData() throws Exception {
List<Object[]> rows = new ArrayList<>();
for (String line : Files.readAllLines(Path.of("src/test/resources/employees.csv"))) {
String[] p = line.split(",");
rows.add(new Object[]{p[0].trim(), p[1].trim(), Double.parseDouble(p[2].trim())});
}
return rows.toArray(new Object[0][]);
}
}

Testte kullanımı

@Test(dataProvider = "employeeData", dataProviderClass = CsvDataProviders.class)
public void bulkInsertEmployees(String name, String job, double salary) throws Exception {
try (var ps = txConn.prepareStatement("INSERT INTO employees(name, job, salary) VALUES (?, ?, ?)")) {
ps.setString(1, name);
ps.setString(2, job);
ps.setDouble(3, salary);
assertEquals(ps.executeUpdate(), 1);
}
}

employees.csv (örnek)

Ali,QA Engineer,7000
Veli,Developer,11000
Ayse,Product Owner,15000

  • Bozuk Foreign Key: Olmayan department_id ile insert → hata beklenir
  • Unique İhlali: Aynı e-posta ile iki kez insert → constraint violation
  • SQL Injection Koruması: PreparedStatement kullanımı zorunlu
  • Transaction Timeout: Uzun süren işlemlerde rollback/süre aşımları
  • Bozuk Veri Tipi: salary='abc' → type mismatch hatası beklenir
@Test(expectedExceptions = java.sql.SQLIntegrityConstraintViolationException.class)
public void shouldFailOnUniqueViolation() throws Exception {
try (var ps = txConn.prepareStatement("INSERT INTO users(email) VALUES (?)")) {
ps.setString(1, "same@mail.com");
ps.executeUpdate();
ps.setString(1, "same@mail.com");
ps.executeUpdate(); // unique key ihlali
}
}

Basit tablo şeması (MySQL)

CREATE TABLE IF NOT EXISTS employees (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
job VARCHAR(100) NOT NULL,
salary DECIMAL(15,2) NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(150) NOT NULL UNIQUE
);

Test başlangıcında şema oluşturma (isteğe bağlı)

// BaseDbTest.startTransaction() içinde çağrılabilir
txConn.createStatement().execute("CREATE TABLE IF NOT EXISTS ...");

  • EXPLAIN ile sorgu planı inceleme
  • Index doğrulamaları
  • Toplam test süresi metrikleri (Sure > threshold → uyarı)
  • Connection Pool metrikleri (HikariCP MBean’leri ile)
EXPLAIN SELECT * FROM employees WHERE job='QA Engineer';

docker-compose.yml (MySQL örneği)

version: "3.8"
services:
db:
image: mysql:8.4
environment:
- MYSQL_DATABASE=testdb
- MYSQL_USER=test_user
- MYSQL_PASSWORD=secret
- MYSQL_ROOT_PASSWORD=root
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 20

GitHub Actions (özet)

name: DB Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.4
env:
MYSQL_DATABASE: testdb
MYSQL_USER: test_user
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h localhost"
--health-interval=5s
--health-timeout=5s
--health-retries=20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Wait DB
run: |
for i in {1..30}; do
(echo > /dev/tcp/127.0.0.1/3306) && break
sleep 2
done
- name: Run tests
run: mvn -B -DskipTests=false test

  • autoCommit=true test izolasyonunu bozar → false + rollback kullanın
  • Her testte aynı veriye dayanmayın; fixture üretin
  • PreparedStatement dışına çıkmayın → enjeksiyon riskine hayır
  • Concurrency testleri için transaction isolation level’ları keşfedin
  • Time zone/LocalDateTime alanlarında JDBC sürücü ayarlarına dikkat

JDBC ile otomasyon testleri, uygulamanın veritabanına doğrudan güven kazandırır.
Transaction + rollback yaklaşımıyla hızlı, izole ve tekrar çalıştırılabilir testler elde edilir.