📝 Giriş
Modern mobil test otomasyonunda, testlerin hızlı ve güvenilir şekilde çalışması kritik önem taşır. Özellikle birden fazla cihazda aynı anda test koşmak (paralel execution), test süresini dramatik şekilde azaltır. Ancak bu yaklaşım, thread-safety (thread güvenliği) konusunda ciddi zorluklar getirir.
Bu yazıda, gerçek bir enterprise projede kullanılan ThreadLocal tabanlı TestContext yönetimi ve paralel test execution mimarisini detaylı olarak inceleyeceğiz. Bu mimari sayesinde:
- ✅ 5-6 farklı cihazda (Android/iOS) aynı anda test koşabilirsiniz
- ✅ Her thread'in izole context'i olur - veri karışması (race condition) yaşanmaz
- ✅ Her cihaz için farklı: Driver, Config, Device parametreleri, Test Data
- ✅ Thread-safety, instability kaynaklarından birini ortadan kaldırır
- ✅ Test süresi %70'e kadar azalır
🎯 Problem: Paralel Testlerde Neden Thread Safety Gerekir?
Senaryo: 5 Android Cihazda Paralel Test
Diyelim ki elimizde 5 Android cihaz var ve aynı test senaryolarını paralel çalıştırmak istiyoruz:
Thread-1 → Emulator-5554 (Port: 4723) → User: JOHN
Thread-2 → Emulator-5556 (Port: 4724) → User: ALFRED
Thread-3 → Emulator-5558 (Port: 4725) → User: BROWN
Thread-4 → Samsung S24 (Port: 4726) → User: CHARLIE
Thread-5 → Iphone 14 (Port: 4727) → User: STEVE
❌ Eğer thread-safe olmayan bir yapı kullanırsanız:
// ❌ YANLIŞ: Static singleton AppiumDriver
public class DriverManager {
private static AppiumDriver driver; // TÜM THREAD'LER BUNU PAYLAŞIR!
public static AppiumDriver getDriver() {
return driver; // Race condition!
}
}
Ne olur?
- Thread-1, Emulator-5554'e bağlanır
- Thread-2, aynı driver değişkenine Samsung S24'ü yazar
- Thread-1, artık yanlış cihaza (Samsung S24) komut gönderir! 💥
- Test data karışır, driver session'lar çakışır
- Sonuç: %30-40 başarısız test, güvenilmez sonuçlar
✅ Çözüm: ThreadLocal ile İzole Context Yönetimi
- ThreadLocal Nedir? ThreadLocal, Java'nın her thread için izole değişken sağlayan mekanizmasıdır.
// Her thread için ayrı bir değer tutar
private static final ThreadLocal<AppiumDriver> driver = new ThreadLocal<>();
// Thread-1 set eder → sadece Thread-1 görür
driver.set(androidDriver1);
// Thread-2 set eder → sadece Thread-2 görür
driver.set(androidDriver2);
// Thread-1 get eder → kendi driver'ını alır
AppiumDriver myDriver = driver.get(); // androidDriver1
Analoji: Her thread'in kendi dolabı var. Thread-1 dolabına koyduğu eşyayı Thread-2 göremez.
🏗️ Mimari: 3 Katmanlı Context Yönetimi
Katman 1: TestContextManager (Singleton-like utility (stateless) + ThreadLocal)
Görev: Her thread için izole TestContext instance'ı yönetir.
package core.context;
public class TestContextManager {
// ✅ ThreadLocal: Her thread için ayrı TestContext
private static final ThreadLocal<TestContext> context = new ThreadLocal<>();
// Thread'in context'ini al
public static TestContext getContext() {
return context.get();
}
// Thread'e context ata
public static void setContext(TestContext testContext) {
if (testContext == null) {
throw new IllegalArgumentException("TestContext cannot be null");
}
context.set(testContext);
}
// Thread bitince temizle (Memory Leak önleme)
public static void removeContext() {
context.remove();
}
// Driver'ı kapat ve context'i temizle
public static void cleanUp() {
TestContext testContext = context.get();
if (testContext != null) {
testContext.cleanUp(); // Driver.quit()
}
context.remove();
}
}
✨ Özellikler:
Singleton-like utility (stateless) pattern: Sadece 1 TestContextManager instance'ı
ThreadLocal storage: Her thread'in kendi TestContext kutusu
Memory leak prevention: removeContext() ile ThreadLocal temizlenir
Katman 2: TestContext (Her Thread'in Kendi Context'i)
Görev: Driver, Config, Device bilgilerini ve Test Data'yı tutar.
package core.context;
import io.appium.java_client.AppiumDriver;
import java.util.HashMap;
import java.util.Map;
public class TestContext {
// ✅ ThreadLocal: Her thread için ayrı driver
private final ThreadLocal<AppiumDriver> driver = new ThreadLocal<>();
// ✅ Immutable: Thread'ler arası paylaşılan sabit veriler
private final ConfigContext configContext; // ENV config (PREPROD/PROD)
private final DeviceContext deviceContext; // Device parametreleri (UDID, port)
// ✅ Mutable: Thread'in kendi test data'sı
public final Map<String, String> testData; // Senaryo içi değişkenler
// ✅ Thread-specific durumlar
private boolean isLoggedIn = false;
private boolean appLaunched = false;
// Constructor: Immutable context'ler alınır
public TestContext(ConfigContext configContext, DeviceContext deviceContext) {
if (configContext == null || deviceContext == null) {
throw new IllegalArgumentException("ConfigContext and DeviceContext cannot be null");
}
this.configContext = configContext;
this.deviceContext = deviceContext;
this.testData = new HashMap<>();
}
// Driver getter/setter (ThreadLocal)
public AppiumDriver getDriver() {
return driver.get();
}
public void setDriver(AppiumDriver d) {
driver.set(d);
}
// Cleanup
public void cleanUp() {
if (driver.get() != null) {
driver.get().quit();
}
}
// Config ve Device bilgilerine erişim
public ConfigContext getConfigContext() { return configContext; }
public DeviceContext getDeviceContext() { return deviceContext; }
}
✨ Anahtar Noktalar:
ThreadLocal Driver: Her thread'in kendi AppiumDriver instance'ı
Immutable Contexts: ConfigContext ve DeviceContext değiştirilemez (thread-safe)
Mutable Test Data: testData Map, sadece o thread tarafından kullanılır
State Management: isLoggedIn, appLaunched gibi thread-specific durumlar
Katman 3: ConfigContext & DeviceContext (Immutable Data Holders)
ConfigContext: Ortam Konfigürasyonu
package core.context;
import java.util.Map;
public class ConfigContext {
// ✅ Final: Değiştirilemez
private final Map<String, String> configData;
public ConfigContext(Map<String, String> configData) {
this.configData = configData;
}
public String getValue(String key) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Config key cannot be null or empty.");
}
String value = this.configData.get(key);
if (value == null) {
throw new RuntimeException("Configuration key '" + key + "' not found in ConfigContext.");
}
return value;
}
}
DeviceContext: Cihaz Parametreleri
package core.context;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class DeviceContext {
// ✅ Unmodifiable Map: Dışarıdan değiştirilemez
private final Map<String, String> deviceParameters;
public DeviceContext(Map<String, String> deviceParameters) {
if (deviceParameters == null || deviceParameters.isEmpty()) {
throw new IllegalArgumentException("Device parameters cannot be null or empty");
}
// Deep copy + unmodifiable
this.deviceParameters = Collections.unmodifiableMap(new HashMap<>(deviceParameters));
}
public String getParameter(String parameterName) {
if (parameterName == null || parameterName.isEmpty()) {
throw new IllegalArgumentException("Parameter name cannot be null or empty");
}
String value = this.deviceParameters.get(parameterName);
if (value == null) {
throw new IllegalArgumentException("No parameter found with name: " + parameterName);
}
return value;
}
}
🎯 Neden Immutable?
Immutable (değiştirilemez) objeler:
- Thread-safe'dir (race condition yok)
- Defensive programming sağlar
- Bug'ları azaltır (unexpected mutation yok)
🚀 Paralel Test Execution: TestNG Integration
- TestNG Suite XML Yapılandırması
Parallel_All_Devices_Suite.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<!-- ✅ parallel="tests" : Her <test> tag'i ayrı thread'de çalışır -->
<!-- ✅ thread-count="5" : Maksimum 5 thread aynı anda -->
<suite name="Parallel-All-Devices-Test-Suite" parallel="tests" thread-count="5">
<listeners>
<!-- ✅ Her <test> context’i başlarken TestListener çalışır -->
<listener class-name="core.listener.TestListener"/>
<listener class-name="core.listener.ExecutionListener"/>
</listeners>
<!-- THREAD 1: Emulator-5554 -->
<test name="Android-Emulator-5554">
<parameter name="cucumber.filter.tags" value="@E5554"/>
<parameter name="USER" value="JOHN"/>
<parameter name="platformName" value="Android"/>
<parameter name="udid" value="emulator-5554"/>
<parameter name="platformVersion" value="16"/>
<parameter name="deviceName" value="emulator-5554"/>
<!-- ✅ Farklı Appium port (aynı port'ta çakışma olmaz) -->
<parameter name="appium.port" value="4723"/>
<parameter name="appium.server.url" value="http://localhost"/>
<classes>
<class name="cucumber.runners.E5554_TestRunner"/>
</classes>
</test>
<!-- THREAD 2: Emulator-5556 -->
<test name="Android-Emulator-5556">
<parameter name="cucumber.filter.tags" value="@E5556"/>
<parameter name="USER" value="ALFRED"/>
<parameter name="platformName" value="Android"/>
<parameter name="udid" value="emulator-5556"/>
<parameter name="platformVersion" value="16"/>
<parameter name="deviceName" value="emulator-5556"/>
<parameter name="appium.port" value="4724"/>
<parameter name="appium.server.url" value="http://localhost"/>
<classes>
<class name="cucumber.runners.E5556_TestRunner"/>
</classes>
</test>
<!-- THREAD 3, 4, 5 benzer şekilde... -->
</suite>
✨ Kritik Noktalar:
1 - parallel="tests": Her tag'i farklı thread'de çalışır
2 - thread-count="5": TestNG pool'da maksimum 5 thread olur
3 - Farklı Appium port'ları: Her cihaz farklı Appium server'a bağlanır
4 - UDID'ler farklı: Her thread farklı cihaza komut gönderir
5 - Farklı USER: Her thread farklı test kullanıcısı kullanır
2. TestListener: Her Thread için Context Kurulumu
package core.listener;
import core.context.*;
import core.driver.DriverManager;
import io.appium.java_client.AppiumDriver;
import org.testng.ITestContext;
import org.testng.ITestListener;
public class TestListener implements ITestListener {
@Override
public void onStart(ITestContext context) {
String testName = context.getName();
// ✅ TestNG XML'den parametreleri al
String userName = context.getCurrentXmlTest().getParameter("USER");
String environment = context.getCurrentXmlTest().getParameter("ENV");
String appiumPort = context.getCurrentXmlTest().getParameter("appium.port");
String appiumServerUrl = context.getCurrentXmlTest().getParameter("appium.server.url");
String udid = context.getCurrentXmlTest().getParameter("udid");
String deviceName = context.getCurrentXmlTest().getParameter("deviceName");
logger.info(">>> THREAD-{} STARTING: {} (Device: {}) <<<",
Thread.currentThread().threadId(), testName, deviceName);
// ✅ 1. Device parametrelerini topla
Map<String, String> deviceParams = new HashMap<>();
deviceParams.put("user", userName);
deviceParams.put("env", environment);
deviceParams.put("udid", udid);
deviceParams.put("deviceName", deviceName);
deviceParams.put("platformName", context.getCurrentXmlTest().getParameter("platformName"));
deviceParams.put("platformVersion", context.getCurrentXmlTest().getParameter("platformVersion"));
// ✅ 2. AppiumDriver oluştur (bu thread için)
String fullAppiumUrl = appiumServerUrl + ":" + appiumPort;
DesiredCapabilities caps = DesiredCapabilitiesUtil.createCapabilities(deviceParams);
AppiumDriver driver = DriverManager.createDriver(fullAppiumUrl, caps);
// ✅ 3. Config ve Context oluştur
Map<String, String> configData = ConfigHelper.getConfigMap(environment, userName);
configData.put("appium.server.url", fullAppiumUrl);
TestContext testContext = new TestContext(
new ConfigContext(configData),
new DeviceContext(deviceParams)
);
// ✅ 4. Driver'ı context'e ata
testContext.setDriver(driver);
// ✅ 5. Thread'e context'i ata (ThreadLocal)
TestContextManager.setContext(testContext);
logger.info("✅ THREAD-{} READY: Driver initialized for {}",
Thread.currentThread().threadId(), deviceName);
}
@Override
public void onFinish(ITestContext context) {
logger.info("<<< THREAD-{} FINISHING: {} >>>",
Thread.currentThread().threadId(), context.getName());
// ✅ Driver'ı kapat ve context'i temizle
TestContext testContext = TestContextManager.getContext();
if (testContext != null) {
AppiumDriver driver = testContext.getDriver();
if (driver != null) {
driver.quit();
}
}
// ✅ ThreadLocal'ı temizle (Memory leak önleme)
TestContextManager.removeContext();
logger.info("✅ THREAD-{} CLEANED UP", Thread.currentThread().threadId());
}
}
🎯 Akış Detayı: Execution Lifecycle
graph TD
A["🟢 TestNG Thread-1 Başlar"] --> B["📋 TestListener.onStart() çalışır"]
B --> C["🔧 Thread-1 için Driver oluştur<br/>Emulator-5554, Port: 4723"]
C --> D["📦 Thread-1 için TestContext oluştur"]
D --> E["🔐 TestContextManager.setContext<br/>ThreadLocal'a kaydet"]
E --> F["🎬 Cucumber senaryoları çalışır<br/>Thread-1'in context'i ile"]
F --> G["🔚 TestListener.onFinish() çalışır"]
G --> H["🧹 Driver.quit() → Context temizle<br/>ThreadLocal.remove()"]
H --> I["⭕ Thread-1 biter"]
style A fill:#90EE90
style I fill:#FFB6C6
style E fill:#87CEEB
style F fill:#FFD700
Aynı Anda :
Thread-2 → Emulator-5556 (Port: 4724)
Thread-3 → Emulator-5558 (Port: 4725)
Thread-4 → Samsung S24 (Port: 4727)
Thread-5 → Iphone 15 (Port: 4728)
🔑 Anahtar Nokta: Her thread kendi driver, kendi context, kendi test data'sı ile izole şekilde çalışır!
🔐 Race Condition'suz Test Data Paylaşımı
Problem: Senaryo İçinde Veri Paylaşımı
Scenario: Para transferi
Given Kullanıcı giriş yapar
When "FROMACCOUNT" hesabından bakiye sorgulanır # 1000 TL
And Bakiye context'e kaydedilir # testData["balance"] = "1000"
And 500 TL transfer yapılır
Then Bakiye "500" TL olmalıdır # testData["balance"] == "500" ?
❌ Yanlış Yaklaşım: Static Map
// ❌ TÜM THREAD'LER AYNI MAP'İ PAYLAŞIR!
public class DataHolder {
private static Map<String, String> testData = new HashMap<>();
}
✅ Doğru Yaklaşım: TestContext İçinde Map
// TestContext.java
public class TestContext {
// ✅ Her thread'in kendi map'i
public final Map<String, String> testData = new HashMap<>();
}
// Step Definition'da kullanım
@When("{string} hesabından bakiye sorgulanır")
public void queryBalance(String accountKey) {
TestContext context = TestContextManager.getContext();
String accountNumber = context.getConfigContext().getValue(accountKey);
// API'den bakiye al
String balance = ApiHelper.getBalance(accountNumber);
// ✅ Bu thread'in context'ine kaydet
context.testData.put("balance", balance);
logger.info("Thread-{}: Balance = {}", Thread.currentThread().threadId(), balance);
}
@Then("Bakiye {string} TL olmalıdır")
public void verifyBalance(String expectedBalance) {
TestContext context = TestContextManager.getContext();
// ✅ Bu thread'in context'inden oku
String actualBalance = context.testData.get("balance");
Assert.assertEquals(actualBalance, expectedBalance);
}
🎯 Sonuç:
Thread-1 → testData["balance"] = "1000" (JOHN kullanıcısı)
Thread-2 → testData["balance"] = "500" (ALFRED kullanıcısı)
Thread-3 → testData["balance"] = "2500" (BROWN kullanıcısı)
Her thread kendi verisini okur/yazar. Karışma olmaz!
📊 Pratik Örnek: 5 Cihazda Paralel Test
Maven Komutu
mvn clean test -Dsurefire.suiteXmlFiles=src/test/resources/suites/Parallel_All_5_Devices_Suite.xml
Çalışma Zamanı Log Çıktısı
[INFO] Thread-15 >>> STARTING TEST: Android-Emulator-5554 <<<
[INFO] Thread-16 >>> STARTING TEST: Android-Emulator-5556 <<<
[INFO] Thread-17 >>> STARTING TEST: Android-Emulator-5558 <<<
[INFO] Thread-18 >>> STARTING TEST: Android-Emulator-5560 <<<
[INFO] Thread-19 >>> STARTING TEST: Android-S24-Test <<<
[INFO] Thread-15: Creating driver for emulator-5554 on port 4723
[INFO] Thread-16: Creating driver for emulator-5556 on port 4724
[INFO] Thread-17: Creating driver for emulator-5558 on port 4725
[INFO] Thread-18: Creating driver for emulator-5560 on port 4726
[INFO] Thread-19: Creating driver for R5CX23A18QD on port 4727
[INFO] Thread-15: TestContext created for JOHN
[INFO] Thread-16: TestContext created for ALFRED
[INFO] Thread-17: TestContext created for BROWN
[INFO] Thread-18: TestContext created for CHARLIE
[INFO] Thread-19: TestContext created for STEVE
[INFO] Thread-15: Scenario 'Login Test' STARTED
[INFO] Thread-16: Scenario 'Transfer Test' STARTED
[INFO] Thread-17: Scenario 'Balance Query' STARTED
[INFO] Thread-18: Scenario 'Payment Test' STARTED
[INFO] Thread-19: Scenario 'Profile Update' STARTED
[INFO] Thread-15: testData["username"] = "JOHN"
[INFO] Thread-16: testData["amount"] = "500"
[INFO] Thread-17: testData["balance"] = "1000"
[INFO] Thread-15: Scenario 'Login Test' PASSED ✓
[INFO] Thread-16: Scenario 'Transfer Test' PASSED ✓
[INFO] Thread-17: Scenario 'Balance Query' PASSED ✓
[INFO] Thread-18: Scenario 'Payment Test' PASSED ✓
[INFO] Thread-19: Scenario 'Profile Update' PASSED ✓
[INFO] Thread-15: Driver quit successfully
[INFO] Thread-16: TestContext removed
[INFO] Thread-17: ThreadLocal cleaned up
Dikkat: Her thread kendi adını, kendi cihazını, kendi test data'sını loglayabiliyor!
🛠️ Appium Server Gereksinimleri
Paralel test için her cihaza ayrı Appium server gerekir:
# Terminal 1
appium --port 4723 --base-path /wd/hub
# Terminal 2
appium --port 4724 --base-path /wd/hub
# Terminal 3
appium --port 4725 --base-path /wd/hub
# Terminal 4
appium --port 4726 --base-path /wd/hub
# Terminal 5
appium --port 4727 --base-path /wd/hub
Alternatif: Docker ile Appium Grid
# Terminal 1
version: '3'
services:
appium-1:
image: appium/appium
ports:
- "4723:4723"
appium-2:
image: appium/appium
ports:
- "4724:4723"
# ... 5 instance
⚠️ Dikkat Edilmesi Gerekenler
- Memory Leak Önleme
// ✅ HER ZAMAN ThreadLocal'ı temizle
@Override
public void onFinish(ITestContext context) {
TestContextManager.removeContext(); // ZORUNLU!
}
Neden? ThreadLocal temizlenmezse:
- Thread pool'da thread yeniden kullanılır
- Eski context verisi kalır (memory leak)
- OutOfMemoryError riski artar
- Thread-Safe Collection Kullanımı
// ✅ Thread-safe collections
private static final Map<String, String> cache = new ConcurrentHashMap<>();
private static final Set<String> processed = Collections.synchronizedSet(new HashSet<>());
// ❌ Thread-safe OLMAYAN
private static final Map<String, String> cache = new HashMap<>(); // Race condition!
- Static Değişkenlerden Kaçının
// ❌ Static mutable state (thread-safe değil)
public class TestData {
private static String userName; // TÜM THREAD'LER PAYLAŞIR!
}
// ✅ ThreadLocal veya TestContext içinde
public class TestContext {
private String userName; // Her thread'in kendi userName'i
}
- Sistem Kaynakları
5 paralel thread için minimum gereksinimler:
- CPU: 8 core
- RAM: 16 GB (her thread için ~2-3 GB)
- Disk I/O: SSD önerilir (screenshot, log yazma hızı)
- Network: Her cihaz için stabil bağlantı
📈 Performans Karşılaştırması
Sequential (Tek Cihaz) vs Parallel (5 Cihaz)
| Metrik | Sequential | Parallel | İyileşme |
|---|---|---|---|
| Test Sayısı | 100 senaryo | 100 senaryo | - |
| Toplam Süre | 45 dakika | 12 dakika | 73% daha hızlı |
| Ortalama Senaryo Süresi | 27 saniye | 27 saniye | Aynı |
| CPU Kullanımı | %25 | %85 | 3.4x artış |
| RAM Kullanımı | 4 GB | 12 GB | 3x artış |
| Test Stability | %92 | %90 | Minimal düşüş |
Sonuç: 5 cihazla paralel test, zamanı %73 azaltır ve stability hala %90 civarındadır!
🎓 Junior + Senior için Özet
Junior Seviye Özeti
ThreadLocal nedir?
- Her thread'in kendi dolabı olduğunu düşün
- Thread-1 dolabına koyduğu şeyi Thread-2 göremez
- ThreadLocal.set() ile koy, ThreadLocal.get() ile al
Paralel test neden önemli?
- 5 cihazda aynı anda test → 5 kat hızlı
- Her cihaz farklı Appium port'unda çalışır
- TestNG parallel="tests" ile otomatik thread yönetimi
Kodda neye dikkat etmeli?
- Static değişken kullanma (hepsi paylaşır)
- TestContext'i TestContextManager.getContext() ile al
- Test bittikten sonra removeContext() ile temizle
Senior Seviye Derinlemesine
Mimari Kararlar:
1 - Singleton-like utility (stateless) + ThreadLocal Pattern:
- TestContextManager Singleton-like utility (stateless) (1 instance)
- ThreadLocal ile thread isolation
- Lazy initialization yerine eager initialization (TestListener'da)
2 - Immutable Context Design:
- ConfigContext ve DeviceContext immutable
- Defensive copy + Collections.unmodifiableMap
- Thread-safe by design (synchronized gerekmez)
3 - Memory Management:
- ThreadLocal.remove() ile explicit cleanup
- Thread pool'da thread reuse → leak riski
- cleanUp() metodunda driver.quit() + context.remove()
4 - Race Condition Prevention:
- testData Map Thread-confined olduğu için synchronization gerekmez
- ConcurrentHashMap kullanımı (shared state için)
- Synchronized bloklar yerine lock-free design
5 - TestNG Integration:
- ITestListener.onStart() → context setup
- ITestListener.onFinish() → context cleanup
- parallel="tests" + thread-count="N"
Trade-offs:
| Karar | Avantaj | Dezavantaj |
|---|---|---|
| ThreadLocal kullanımı | Thread isolation, race condition yok | Memory overhead, cleanup gerekli |
| Immutable contexts | Thread-safe, defensive | Flexibility düşük, yeniden oluşturma gerekir |
| TestNG paralel execution | Kolay yapılandırma, ölçeklenebilir | Thread pool sınırlı, resource yoğun |
| Driver per thread | İzolasyon, stability | Appium server sayısı artışı |
🚀 Sonuç ve Best Practices
✅ Öneriler
- Her thread için izole context kullan (ThreadLocal)
- Immutable data structures tercih et (ConfigContext, DeviceContext)
- Thread-safe collections kullan (ConcurrentHashMap, synchronizedSet)
- ThreadLocal'ı MUTLAKA temizle (memory leak önleme)
- Her cihaz için ayrı Appium port (4723, 4724, 4725...)
- Test data'yı TestContext içinde tut (thread'ler arası izolasyon)
- Logging'de thread ID'yi ekle (debug kolaylığı)
- Sistem kaynaklarını hesaba kat (CPU, RAM, disk I/O)
❌ Yapılmaması Gerekenler
- Static mutable state kullanma (race condition)
- Shared driver instance (thread'ler arasında paylaşım)
- ThreadLocal temizlemeyi unutma (memory leak)
- Aynı Appium port'unu kullanma (port conflict)
- Thread-safe olmayan collections (HashMap, ArrayList)
- Synchronized bloklara aşırı bağımlılık (performance bottleneck)
📚 Daha Fazla Okuma
- TestNG Parallel Execution Docs
- Java ThreadLocal Javadoc
- Appium Multi-Session Guide
- Effective Java: Item 83 - Use lazy initialization judiciously
📝 Örnek Proje Yapısı
ttb-qa-mobile-automation/
├── src/test/java/
│ ├── core/
│ │ ├── context/
│ │ │ ├── TestContextManager.java # ⭐ Singleton (stateless) + ThreadLocal
│ │ │ ├── TestContext.java # ⭐ Per-thread context
│ │ │ ├── ConfigContext.java # ⭐ Immutable config
│ │ │ └── DeviceContext.java # ⭐ Immutable device params
│ │ ├── driver/
│ │ │ └── DriverManager.java # Driver oluşturma
│ │ └── listener/
│ │ └── TestListener.java # ⭐ TestNG listener
│ └── cucumber/
│ ├── runners/
│ │ └── AndroidTestRunner.java # Cucumber runner
│ ├── steps/
│ │ └── StepDefinitions.java # Step definitions
│ └── features/
│ └── Login.feature # Gherkin scenarios
└── src/test/resources/
└── suites/
└── Parallel_Android_5_Devices_Suite.xml # ⭐ TestNG suite
🎉 Sonuç
Bu yazıda, ThreadLocal tabanlı context yönetimi ve paralel test execution mimarisini detaylı olarak inceledik. Bu yaklaşım sayesinde:
- ✅ 5-6 cihazda aynı anda test koşabilirsiniz
- ✅ Thread-safe yapı ile race condition'dan kaçınırsınız
- ✅ Test süresini %70'e kadar azaltırsınız
- ✅ %90+ stability ile güvenilir sonuçlar alırsınız
Ana mesaj: Paralel test, doğru mimari ile hem hızlı hem de güvenilir olabilir. ThreadLocal ve immutable design patterns, bu mimarinin temel taşlarıdır.
Sorularınız için: GitHub Issues veya yorumlar bölümünden ulaşabilirsiniz!
Yazar: Ümit Sinanoğlu
Tarih: 16 Aralık 2025
Versiyon: 1.0
Etiketler: #MobileAutomation #ThreadLocal #ParallelTesting #Appium #TestNG #Java
Top comments (0)