Waits y sincronización: reemplazando Thread.sleep por WebDriverWait en Appium
5 Thread.sleep eliminados, WebDriverWait con ExpectedConditions, un error de driver duplicado y comparación de tiempos. 4 tests, 0 sleeps.
El problema con Thread.sleep
El Post anterior sobre Interacciones básicas: tap, input, scroll y assertions en Appium terminó con 4 tests verdes y 5 Thread.sleep en el código. Funcionaba, pero cada Thread.sleep es un problema:
// esperar 1 segundo a ciegas
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000);
Tres problemas concretos:
Thread.sleep(1000) espera 1 segundo siempre. Si el menú tarda 200ms en abrirse, desperdicias 800ms. Si tarda 1200ms, el test falla. No hay forma de ganar.
Segundo: Thread.sleep obliga a declarar throws InterruptedException en cada método. Es ruido en la firma que no aporta nada.
Tercero: en una suite grande, los segundos se acumulan. 5 sleeps de 1-3 segundos en 4 tests. Multiplicá eso por 50 tests y perdés minutos esperando a la nada.
En Selenium pasa exactamente lo mismo. Thread.sleep es el primer mecanismo que aprendés y el primero que deberías reemplazar.
WebDriverWait: esperar lo que necesitás
La solución es WebDriverWait con ExpectedConditions. En vez de esperar un tiempo fijo, esperás hasta que una condición se cumpla.
// esperar hasta que el elemento sea visible
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("Login Menu Item")
));
wait.until pregunta cada 500ms: "¿el elemento es visible?". Si aparece en 200ms, continúa. Si no aparece en 10 segundos, lanza TimeoutException. No espera de más ni falla por poco.
Setup del WebDriverWait
Dos cambios en el código. Primero, declarar el wait como variable de instancia al lado del driver:
private AndroidDriver driver;
private WebDriverWait wait;
Segundo, inicializarlo en setUp después de crear el driver:
driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
Duration.ofSeconds(10) es el timeout máximo. Si el elemento no aparece en 10 segundos, el test falla. Para esta app, 10 es más que suficiente.
Imports necesarios
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
Son los mismos imports que en Selenium. WebDriverWait y ExpectedConditions vienen del paquete org.openqa.selenium.support.ui — Appium los hereda directamente.
Error: crear el driver dos veces
Cuando agregué el WebDriverWait al setUp, cometí un error. Inicialicé el wait entre dos bloques de options y terminé creando el driver dos veces:
// MAL: dos drivers
driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options); // primer driver
wait = new WebDriverWait(driver, Duration.ofSeconds(10)); // wait apunta a este
options.setNewCommandTimeout(Duration.ofSeconds(120)); // más options...
options.setCapability("appium:appWaitActivity", "*");
driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options); // segundo driver (pisa al primero)
El problema: wait queda apuntando al primer driver, que ya fue reemplazado. Las esperas corren contra una sesión que no es la activa.
La solución: todas las options primero, un solo new AndroidDriver, y después el wait:
// BIEN: un solo driver
UiAutomator2Options options = new UiAutomator2Options();
options.setUdid("ZY32FJFXNF");
options.setApp(new File("apk/mda-2.2.0-25.apk").getAbsolutePath());
options.setAutomationName("UiAutomator2");
options.setNewCommandTimeout(Duration.ofSeconds(120));
options.setCapability("appium:appWaitActivity", "*");
driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
Un error fácil de cometer cuando el setup tiene muchas líneas de configuración.. En Appium, el setup tiene más líneas y es más fácil meter código en el medio donde no va.
Reemplazando cada Thread.sleep
Fui test por test. El patrón es siempre el mismo: donde había un Thread.sleep, ahora espero al elemento que necesito para el siguiente paso.
loginExitoso — 3 sleeps eliminados
Tenía 3: uno después del menú hamburguesa, uno después de Login Menu Item, uno después del tap en Login.
// Antes
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
Thread.sleep(1000);
// Después
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("Login Menu Item")
));
La lógica: después de tocar el menú hamburguesa, ¿qué necesito? Que "Login Menu Item" sea visible. Eso es lo que espero. No un segundo, no dos — hasta que el elemento esté ahí.
Lo mismo para la espera después del login:
// Antes
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
Thread.sleep(3000);
// Después
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("title")
));
Este era el sleep más largo (3 segundos). Esperaba a que Products cargara después del login. Ahora espera al título "Products" — si aparece en 500ms, avanza. Si tarda 8 segundos por alguna razón, también funciona.
loginConClearYReingreso — 3 sleeps eliminados
Mismo patrón: reemplacé los 2 sleeps de navegación y el sleep después del login.
loginConCamposVacios — 2 sleeps eliminados
Los 2 sleeps de navegación al login. Este test no tenía sleep después del tap porque la validación de campos vacíos es instantánea — la app muestra el error sin transición de pantalla.
scrollHastaProducto — sin cambios
Este test nunca tuvo Thread.sleep. UiScrollable ya maneja su propia sincronización internamente.
throws InterruptedException: limpieza
Sin Thread.sleep, los métodos ya no lanzan InterruptedException. Saqué el throws de los 4 tests:
// Antes
public void loginExitoso() throws InterruptedException {
// Después
public void loginExitoso() {
Parece menor, pero es una señal de que el código está más limpio. Si un método de test necesita declarar excepciones checked, algo huele mal.
Comparación de tiempos
Corrí los 4 tests dos veces: una con la versión Thread.sleep, otra con WebDriverWait.


| Test | Thread.sleep | WebDriverWait | Diferencia |
|---|---|---|---|
| loginExitoso | 7.6s | 5.5s | -2.1s |
| loginConClearYReingreso | 10.8s | 6.6s | -4.2s |
| scrollHastaProducto | 13.7s | 11.7s | -2.0s |
| loginConCamposVacios | 3.7s | 5.2s | +1.5s |
| Total | 2:24 | 2:17 | -7s |
loginConClearYReingreso bajó 4 segundos — era el que tenía más sleeps acumulados (3 en total). loginConCamposVacios subió un poco, varianza normal del dispositivo, no del código.
7 segundos menos en 4 tests. En una suite de 50 tests eso escala a minutos reales de ejecución.
Pero la ganancia no es solo velocidad. Es estabilidad. Un Thread.sleep(1000) falla si la app tarda 1200ms. Un wait.until con timeout de 10 segundos tolera variaciones de carga, animaciones lentas, dispositivos más viejos.
Paralelo con Selenium
| Concepto | Selenium (web) | Appium (mobile) |
|---|---|---|
| Wait explícito | WebDriverWait |
WebDriverWait (mismo) |
| Condiciones | ExpectedConditions |
ExpectedConditions (mismo) |
| Import | org.openqa.selenium.support.ui |
org.openqa.selenium.support.ui (mismo) |
| Timeout por defecto | No hay | No hay |
| Implicit wait | driver.manage().timeouts().implicitlyWait() |
Igual, pero NO recomendado con explicit waits |
Thread.sleep |
Mala práctica | Mala práctica |
| Diferencia clave | — | Mobile tiene más animaciones, transiciones y tiempos variables |
Si venís de Selenium, WebDriverWait funciona exactamente igual. No hay nada nuevo que aprender. La única diferencia es que en mobile lo necesitás más — las apps tienen transiciones, animaciones y tiempos de carga más variables que una página web.
¿Y FluentWait?
FluentWait es una versión más configurable de WebDriverWait. Permite cambiar el intervalo de polling (cada cuánto revisa la condición) y qué excepciones ignorar.
FluentWait<AndroidDriver> fwait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(15))
.pollingEvery(Duration.ofMillis(300))
.ignoring(NoSuchElementException.class);
No lo usé acá. WebDriverWait con el polling por defecto de 500ms es suficiente para esta app. FluentWait tiene sentido cuando necesitás control más fino — polling más rápido, o ignorar excepciones específicas durante la espera.
Lo menciono porque aparece en entrevistas. La pregunta clásica: "¿cuál es la diferencia entre WebDriverWait y FluentWait?". La respuesta: WebDriverWait es un FluentWait con configuración por defecto. Internamente, WebDriverWait extiende de FluentWait.
ExpectedConditions más usadas
Usé visibilityOfElementLocated para todo en este post. Pero hay otras:
elementToBeClickable — espera que el elemento sea visible y habilitado. Útil para botones que aparecen grayed out mientras carga algo.
presenceOfElementLocated — espera que el elemento exista en el DOM, aunque no sea visible. Más rápido que visibility, pero no garantiza que el usuario pueda verlo.
invisibilityOfElementLocated — espera que un elemento desaparezca. Perfecto para loaders o spinners: "esperá a que el spinner se vaya".
textToBePresentInElement — espera que un elemento tenga un texto específico. Útil cuando el contenido se carga dinámicamente.
Para este post, visibilityOfElementLocated es la correcta. Quiero saber que el elemento está visible en pantalla antes de interactuar. Las otras las voy a usar cuando el flujo lo requiera.
Estado actual
4 tests, 0 Thread.sleep, WebDriverWait centralizado en setUp.
| Lo que cambió | Post 5 | Post 6 |
|---|---|---|
| Mecanismo de espera | Thread.sleep (5 instancias) |
WebDriverWait (0 sleeps) |
| Excepciones en tests | throws InterruptedException |
Ninguna |
| Tiempo total (4 tests) | 2:24 | 2:17 |
| Estabilidad | Frágil (tiempos fijos) | Robusta (condiciones reales) |
Código completo
import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.io.File;
import java.net.URL;
import java.time.Duration;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.By;
public class LoginTest {
private AndroidDriver driver;
private WebDriverWait wait;
@BeforeMethod
public void setUp() throws Exception {
UiAutomator2Options options = new UiAutomator2Options();
options.setUdid("ZY32FJFXNF");
options.setApp(new File("apk/mda-2.2.0-25.apk").getAbsolutePath());
options.setAutomationName("UiAutomator2");
options.setNewCommandTimeout(Duration.ofSeconds(120));
options.setCapability("appium:appWaitActivity", "*");
driver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
@Test
public void loginExitoso() {
// 1. Tap en menú hamburguesa
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("Login Menu Item")
));
// 2. Tap en "Log In" dentro del menú
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")
));
// 3. Ingresar username
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("[email protected]");
// 4. Ingresar password
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/passwordET")).sendKeys("10203040");
// 5. Tap en botón Login
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 6. Esperar que cargue Products
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("title")
));
// 7. Verificar que volvió a Products
String titulo = driver.findElement(AppiumBy.accessibilityId("title")).getText();
Assert.assertEquals(titulo, "Products", "No volvió a la pantalla de Products después del login");
}
@Test
public void scrollHastaProducto() {
// Scroll hasta un producto que no está visible
driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true))" +
".scrollTextIntoView(\"Sauce Labs Onesie\")"
));
// Verificar que el producto es visible
String producto = driver.findElement(AppiumBy.androidUIAutomator(
"new UiSelector().text(\"Sauce Labs Onesie\")"
)).getText();
Assert.assertEquals(producto, "Sauce Labs Onesie", "No encontró el producto después del scroll");
}
@Test
public void loginConClearYReingreso() {
// 1. Ir al login
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("Login Menu Item")
));
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")
));
// 2. Ingresar username incorrecto
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("usuario_equivocado");
// 3. Limpiar y poner el correcto
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).clear();
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")).sendKeys("[email protected]");
// 4. Password y login
driver.findElement(AppiumBy.id("com.saucelabs.mydemoapp.android:id/passwordET")).sendKeys("10203040");
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 5. Verificar
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("title")
));
String titulo = driver.findElement(AppiumBy.accessibilityId("title")).getText();
Assert.assertEquals(titulo, "Products");
}
@Test
public void loginConCamposVacios() {
// 1. Ir al login
driver.findElement(AppiumBy.accessibilityId("View menu")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("Login Menu Item")
));
driver.findElement(AppiumBy.accessibilityId("Login Menu Item")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameET")
));
// 2. Tap en Login sin ingresar nada
driver.findElement(AppiumBy.accessibilityId("Tap to login with given credentials")).click();
// 3. Verificar mensaje de error en username
String errorUsername = driver.findElement(
AppiumBy.id("com.saucelabs.mydemoapp.android:id/nameErrorTV")
).getText();
Assert.assertEquals(errorUsername, "Username is required", "No mostró error de username vacío");
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
Repo: github.com/cesarbeassuarez/appium-java-framework
Siguiente en la serie
Page Object Model para mobile. Los 4 tests tienen locators y navegación repetida. POM centraliza eso — misma arquitectura que en Selenium, adaptada a Appium.
Tabla de contenido de la serie
| Post | Tema | Estado |
|---|---|---|
| 1 | Por qué Appium: decisiones técnicas | Publicado |
| 2 | Setup completo: Node.js, Android Studio, Appium Server y emulador | Publicado |
| 3 | Primer test: abrir la app en el emulador | Publicado |
| 4 | Appium Inspector: encontrar elementos con dispositivo físico | Publicado |
| 5 | Interacciones básicas: tap, input, scroll, assertions | Publicado |
| 6 | Waits y sincronización en mobile | ← Este post |
| 7 | Page Object Model para mobile | Siguiente |