Java ve JDBC ile Database Otomasyon Testi
1) Giriş
Section titled “1) Giriş”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.
2) Mimari ve En İyi Pratikler
Section titled “2) Mimari ve En İyi Pratikler”- 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
3) Proje Kurulumu (Maven)
Section titled “3) Proje Kurulumu (Maven)”<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.java4) Test Konfigürasyonu
Section titled “4) Test Konfigürasyonu”application-test.properties
db.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=truedb.username=test_userdb.password=secretdb.pool.maxSize=5DataSourceFactory.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; }}5) Basit DAO ve JDBC Örneği
Section titled “5) Basit DAO ve JDBC Örneği”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.7) Örnek Testler (TestNG)
Section titled “7) Örnek Testler (TestNG)”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); } }}8) Data Provider ile Veri Odaklı Test
Section titled “8) Data Provider ile Veri Odaklı Test”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,7000Veli,Developer,11000Ayse,Product Owner,150009) Negatif Testler ve Güvenlik
Section titled “9) Negatif Testler ve Güvenlik”- Bozuk Foreign Key: Olmayan
department_idile 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 }}10) Şema ve Fixture Yönetimi
Section titled “10) Şema ve Fixture Yönetimi”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ılabilirtxConn.createStatement().execute("CREATE TABLE IF NOT EXISTS ...");11) Performans ve İzleme
Section titled “11) Performans ve İzleme”- 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';12) CI ve Docker ile Yerel DB
Section titled “12) CI ve Docker ile Yerel DB”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: 20GitHub Actions (özet)
name: DB Testson: [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 test13) Sık Hatalar ve İpuçları
Section titled “13) Sık Hatalar ve İpuçları”autoCommit=truetest 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/
LocalDateTimealanlarında JDBC sürücü ayarlarına dikkat
14) Sonuç
Section titled “14) Sonuç”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.