QA Interview Hub · База знань · Тести

Java: основи

Паралельність

ThreadLocal<WebDriver> і спільний стан у паралельних тестах

Паралельний запуск скорочує час прогону, але всі потоки одного JVM ділять статичні поля й об’єкти. Тому тест має володіти власним станом. Три рівні рішення:

  1. Нестатичні поля — коли раннер створює окремий екземпляр тестового класу для кожного тесту (JUnit 5 за замовчуванням). Для TestNG екземпляр один на клас, тому поле спільне для його методів.
  2. ThreadLocal — кожен потік бачить власне значення змінної:
    public final class DriverHolder {
        private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
        public static WebDriver get() { return DRIVER.get(); }
        public static void set(WebDriver d) { DRIVER.set(d); }
        public static void quit() { DRIVER.get().quit(); DRIVER.remove(); }
    }
  3. Окремі дані на тест — унікальні користувачі й сутності, а не спільний акаунт, інакше тести заважають один одному (змінили пароль, видалили товар).

Нюанси ThreadLocal:

  • remove() після тесту обов’язковий: пул повторно використовує потоки, і значення «просочиться» у наступний тест або утримуватиме пам’ять.
  • Дочірній потік не бачить значення батьківського (для цього є InheritableThreadLocal). Якщо тест сам запускає потоки, передавайте драйвер явно.
  • ThreadLocal не робить сам WebDriver потокобезпечним: один драйвер усе одно використовує один потік.

Спільний змінюваний стан (лічильники, HashMap з кешем токенів, список результатів) спричиняє race condition — результат залежить від порядку потоків. Рішення: уникати спільного стану; ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger; або synchronized.

synchronized, volatile, atomic і ExecutorService

Чому count++ небезпечний. Це три кроки: прочитати, збільшити, записати. Два потоки можуть прочитати те саме значення, і одне збільшення загубиться. Варіанти:

  • AtomicInteger / AtomicLong: counter.incrementAndGet() — атомарно й без блокувань; найпростіший вибір для лічильників.
  • synchronized-метод чи блок: лише один потік за раз виконує ділянку коду під одним монітором. Дорожче, а при неправильному порядку захоплення кількох замків можливий deadlock.
  • volatile: гарантує, що зміни поля видно іншим потокам, але не робить складні операції (++) атомарними. Підходить для прапорця «зупинитись».
  • Потокобезпечні колекції (ConcurrentHashMap), а не HashMap/ArrayList зі спільним записом.

ExecutorService — пул потоків для власних паралельних задач у тестах (наприклад, навантажувальний запит із багатьох клієнтів, перевірка гонки двох замовлень):

ExecutorService pool = Executors.newFixedThreadPool(4);
try {
    List<Future<Response>> futures = new ArrayList<>();
    for (int i = 0; i < 10; i++) futures.add(pool.submit(() -> client.createOrder(cart)));
    for (Future<Response> f : futures) assertThat(f.get().status()).isEqualTo(201);
} finally {
    pool.shutdown();
}
  • submit повертає Future; get() чекає результат і кидає ExecutionException з причиною, якщо задача впала. Виняток у потоці пулу інакше нікуди не потрапляє.
  • Пул треба зупинити: shutdown() (і awaitTermination), інакше потоки утримують JVM. Із Java 19 ExecutorService має close(), тож працює try-with-resources.
  • Для get задавайте таймаут: f.get(10, TimeUnit.SECONDS).
  • Java 21 додала віртуальні потоки: Executors.newVirtualThreadPerTaskExecutor() — зручно для багатьох блокуючих HTTP-викликів.