DEV Community

Umit Sinanoglu
Umit Sinanoglu

Posted on

# Mobil Test Otomasyonunda ThreadLocal ile Context Yönetimi: N Adet Cihazda Paralel Test Execution

📝 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
Enter fullscreen mode Exit fullscreen mode

❌ 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!
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. 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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

✨ Ö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; }
}
Enter fullscreen mode Exit fullscreen mode

✨ 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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

  1. 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>
Enter fullscreen mode Exit fullscreen mode

✨ 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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
Enter fullscreen mode Exit fullscreen mode

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" ?
Enter fullscreen mode Exit fullscreen mode

❌ Yanlış Yaklaşım: Static Map

// ❌ TÜM THREAD'LER AYNI MAP'İ PAYLAŞIR!
public class DataHolder {
    private static Map<String, String> testData = new HashMap<>();
}
Enter fullscreen mode Exit fullscreen mode

✅ 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);
}
Enter fullscreen mode Exit fullscreen mode

🎯 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ı)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Ç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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

⚠️ Dikkat Edilmesi Gerekenler

  1. Memory Leak Önleme
// ✅ HER ZAMAN ThreadLocal'ı temizle
@Override
public void onFinish(ITestContext context) {
    TestContextManager.removeContext(); // ZORUNLU!
}
Enter fullscreen mode Exit fullscreen mode

Neden? ThreadLocal temizlenmezse:

  • Thread pool'da thread yeniden kullanılır
  • Eski context verisi kalır (memory leak)
  • OutOfMemoryError riski artar
  1. 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!
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. 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

  1. Her thread için izole context kullan (ThreadLocal)
  2. Immutable data structures tercih et (ConfigContext, DeviceContext)
  3. Thread-safe collections kullan (ConcurrentHashMap, synchronizedSet)
  4. ThreadLocal'ı MUTLAKA temizle (memory leak önleme)
  5. Her cihaz için ayrı Appium port (4723, 4724, 4725...)
  6. Test data'yı TestContext içinde tut (thread'ler arası izolasyon)
  7. Logging'de thread ID'yi ekle (debug kolaylığı)
  8. Sistem kaynaklarını hesaba kat (CPU, RAM, disk I/O)

❌ Yapılmaması Gerekenler

  1. Static mutable state kullanma (race condition)
  2. Shared driver instance (thread'ler arasında paylaşım)
  3. ThreadLocal temizlemeyi unutma (memory leak)
  4. Aynı Appium port'unu kullanma (port conflict)
  5. Thread-safe olmayan collections (HashMap, ArrayList)
  6. Synchronized bloklara aşırı bağımlılık (performance bottleneck)

📚 Daha Fazla Okuma

📝 Ö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

Enter fullscreen mode Exit fullscreen mode

🎉 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)